summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.travis.yml13
-rw-r--r--README.rst4
-rw-r--r--codecov.yml1
-rw-r--r--paramiko/_version.py2
-rw-r--r--paramiko/agent.py4
-rw-r--r--paramiko/auth_handler.py9
-rw-r--r--paramiko/buffered_pipe.py10
-rw-r--r--paramiko/channel.py54
-rw-r--r--paramiko/client.py91
-rw-r--r--paramiko/common.py22
-rw-r--r--paramiko/dsskey.py10
-rw-r--r--paramiko/ecdsakey.py8
-rw-r--r--paramiko/ed25519key.py7
-rw-r--r--paramiko/file.py26
-rw-r--r--paramiko/hostkeys.py11
-rw-r--r--paramiko/kex_ecdh_nist.py118
-rw-r--r--paramiko/kex_gss.py6
-rw-r--r--paramiko/packet.py12
-rw-r--r--paramiko/pkey.py42
-rw-r--r--paramiko/primes.py1
-rw-r--r--paramiko/py3compat.py11
-rw-r--r--paramiko/resource.py71
-rw-r--r--paramiko/rsakey.py8
-rw-r--r--paramiko/server.py67
-rw-r--r--paramiko/sftp_client.py63
-rw-r--r--paramiko/sftp_file.py24
-rw-r--r--paramiko/sftp_handle.py10
-rw-r--r--paramiko/sftp_server.py8
-rw-r--r--paramiko/sftp_si.py32
-rw-r--r--paramiko/ssh_exception.py18
-rw-r--r--paramiko/ssh_gss.py43
-rw-r--r--paramiko/transport.py154
-rw-r--r--paramiko/win_pageant.py2
-rw-r--r--setup.py3
-rw-r--r--sites/www/changelog.rst120
-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.py107
-rwxr-xr-xtests/test_file.py57
-rw-r--r--tests/test_kex.py57
-rw-r--r--tests/test_pkey.py45
-rwxr-xr-xtests/test_sftp.py63
-rw-r--r--tests/test_transport.py78
44 files changed, 1079 insertions, 482 deletions
diff --git a/.travis.yml b/.travis.yml
index 4cacb017..0e46ec84 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -10,21 +10,18 @@ python:
- "3.4"
- "3.5"
- "3.6"
- - "pypy-5.4.1"
+ - "pypy-5.6.0"
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!
- inv test --coverage
- # Ensure documentation & invoke pipeline run OK.
- # Run 'docs' first since its objects.inv is referred to by 'www'.
- # Also force warnings to be errors since most of them tend to be actual
- # problems.
- - invoke docs -o -W www -o -W
+ # Ensure documentation builds, both sites, maxxed nitpicking
+ - inv sites
# flake8 is now possible!
- flake8
notifications:
@@ -36,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/_version.py b/paramiko/_version.py
index 2ad47eb4..c8ca86d1 100644
--- a/paramiko/_version.py
+++ b/paramiko/_version.py
@@ -1,2 +1,2 @@
-__version_info__ = (2, 1, 2)
+__version_info__ = (2, 2, 1)
__version__ = '.'.join(map(str, __version_info__))
diff --git a/paramiko/agent.py b/paramiko/agent.py
index a7cab4d8..bc857efa 100644
--- a/paramiko/agent.py
+++ b/paramiko/agent.py
@@ -253,7 +253,7 @@ class AgentServerProxy(AgentSSH):
"""
:param .Transport t: Transport used for SSH Agent communication forwarding
- :raises SSHException: mostly if we lost the agent
+ :raises: `.SSHException` -- mostly if we lost the agent
"""
def __init__(self, t):
AgentSSH.__init__(self)
@@ -347,7 +347,7 @@ class Agent(AgentSSH):
opened, if one is running. If no agent is running, initialization will
succeed, but `get_keys` will return an empty tuple.
- :raises SSHException:
+ :raises: `.SSHException` --
if an SSH agent is found, but speaks an incompatible protocol
"""
def __init__(self):
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/buffered_pipe.py b/paramiko/buffered_pipe.py
index 9a65cd95..d9f5149d 100644
--- a/paramiko/buffered_pipe.py
+++ b/paramiko/buffered_pipe.py
@@ -90,7 +90,7 @@ class BufferedPipe (object):
Feed new data into this pipe. This method is assumed to be called
from a separate thread, so synchronization is done.
- :param data: the data to add, as a `str` or `bytes`
+ :param data: the data to add, as a ``str`` or ``bytes``
"""
self._lock.acquire()
try:
@@ -134,11 +134,11 @@ class BufferedPipe (object):
:param int nbytes: maximum number of bytes to read
:param float timeout:
maximum seconds to wait (or ``None``, the default, to wait forever)
- :return: the read data, as a `bytes`
+ :return: the read data, as a ``str`` or ``bytes``
- :raises PipeTimeout:
- if a timeout was specified and no data was ready before that
- timeout
+ :raises:
+ `.PipeTimeout` -- if a timeout was specified and no data was ready
+ before that timeout
"""
out = bytes()
self._lock.acquire()
diff --git a/paramiko/channel.py b/paramiko/channel.py
index d295e938..c6016a0e 100644
--- a/paramiko/channel.py
+++ b/paramiko/channel.py
@@ -47,8 +47,9 @@ def open_only(func):
"""
Decorator for `.Channel` methods which performs an openness check.
- :raises SSHException:
- If the wrapped method is called on an unopened `.Channel`.
+ :raises:
+ `.SSHException` -- If the wrapped method is called on an unopened
+ `.Channel`.
"""
@wraps(func)
def _check(self, *args, **kwds):
@@ -166,8 +167,9 @@ class Channel (ClosingContextManager):
:param int width_pixels: width (in pixels) of the terminal screen
:param int height_pixels: height (in pixels) of the terminal screen
- :raises SSHException:
- if the request was rejected or the channel was closed
+ :raises:
+ `.SSHException` -- if the request was rejected or the channel was
+ closed
"""
m = Message()
m.add_byte(cMSG_CHANNEL_REQUEST)
@@ -198,7 +200,8 @@ class Channel (ClosingContextManager):
When the shell exits, the channel will be closed and can't be reused.
You must open a new channel if you wish to open another shell.
- :raises SSHException: if the request was rejected or the channel was
+ :raises:
+ `.SSHException` -- if the request was rejected or the channel was
closed
"""
m = Message()
@@ -223,7 +226,8 @@ class Channel (ClosingContextManager):
:param str command: a shell command to execute.
- :raises SSHException: if the request was rejected or the channel was
+ :raises:
+ `.SSHException` -- if the request was rejected or the channel was
closed
"""
m = Message()
@@ -248,8 +252,9 @@ class Channel (ClosingContextManager):
:param str subsystem: name of the subsystem being requested.
- :raises SSHException:
- if the request was rejected or the channel was closed
+ :raises:
+ `.SSHException` -- if the request was rejected or the channel was
+ closed
"""
m = Message()
m.add_byte(cMSG_CHANNEL_REQUEST)
@@ -272,8 +277,9 @@ class Channel (ClosingContextManager):
:param int width_pixels: new width (in pixels) of the terminal screen
:param int height_pixels: new height (in pixels) of the terminal screen
- :raises SSHException:
- if the request was rejected or the channel was closed
+ :raises:
+ `.SSHException` -- if the request was rejected or the channel was
+ closed
"""
m = Message()
m.add_byte(cMSG_CHANNEL_REQUEST)
@@ -301,9 +307,9 @@ class Channel (ClosingContextManager):
:param dict environment:
a dictionary containing the name and respective values to set
- :raises SSHException:
- if any of the environment variables was rejected by the server or
- the channel was closed
+ :raises:
+ `.SSHException` -- if any of the environment variables was rejected
+ by the server or the channel was closed
"""
for name, value in environment.items():
try:
@@ -326,8 +332,9 @@ class Channel (ClosingContextManager):
:param str name: name of the environment variable
:param str value: value of the environment variable
- :raises SSHException:
- if the request was rejected or the channel was closed
+ :raises:
+ `.SSHException` -- if the request was rejected or the channel was
+ closed
"""
m = Message()
m.add_byte(cMSG_CHANNEL_REQUEST)
@@ -366,11 +373,11 @@ class Channel (ClosingContextManager):
`.Transport` or session's ``window_size`` (e.g. that set by the
``default_window_size`` kwarg for `.Transport.__init__`) will cause
`.recv_exit_status` to hang indefinitely if it is called prior to a
- sufficiently large `~Channel..read` (or if there are no threads
- calling `~Channel.read` in the background).
+ sufficiently large `.Channel.recv` (or if there are no threads
+ calling `.Channel.recv` in the background).
In these cases, ensuring that `.recv_exit_status` is called *after*
- `~Channel.read` (or, again, using threads) can avoid the hang.
+ `.Channel.recv` (or, again, using threads) can avoid the hang.
:return: the exit code (as an `int`) of the process on the server.
@@ -444,8 +451,8 @@ class Channel (ClosingContextManager):
if True, only a single x11 connection will be forwarded (by
default, any number of x11 connections can arrive over this
session)
- :param function handler:
- an optional handler to use for incoming X11 connections
+ :param handler:
+ an optional callable handler to use for incoming X11 connections
:return: the auth_cookie used
"""
if auth_protocol is None:
@@ -474,8 +481,9 @@ class Channel (ClosingContextManager):
Request for a forward SSH Agent on this channel.
This is only valid for an ssh-agent from OpenSSH !!!
- :param function handler:
- a required handler to use for incoming SSH Agent connections
+ :param handler:
+ a required callable handler to use for incoming SSH Agent
+ connections
:return: True if we are ok, else False
(at that time we always return ok)
@@ -666,7 +674,7 @@ class Channel (ClosingContextManager):
length zero is returned, the channel stream has closed.
:param int nbytes: maximum number of bytes to read.
- :return: received data, as a `bytes`
+ :return: received data, as a ``str``/``bytes``.
:raises socket.timeout:
if no data is ready before the timeout set by `settimeout`.
diff --git a/paramiko/client.py b/paramiko/client.py
index c914b8a0..936693fc 100644
--- a/paramiko/client.py
+++ b/paramiko/client.py
@@ -22,6 +22,7 @@ SSH client & key policies
from binascii import hexlify
import getpass
+import inspect
import os
import socket
import warnings
@@ -35,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
@@ -92,7 +92,7 @@ class SSHClient (ClosingContextManager):
:param str filename: the filename to read, or ``None``
- :raises IOError:
+ :raises: ``IOError`` --
if a filename was provided and the file could not be read
"""
if filename is None:
@@ -119,7 +119,7 @@ class SSHClient (ClosingContextManager):
:param str filename: the filename to read
- :raises IOError: if the filename could not be read
+ :raises: ``IOError`` -- if the filename could not be read
"""
self._host_keys_filename = filename
self._host_keys.load(filename)
@@ -132,7 +132,7 @@ class SSHClient (ClosingContextManager):
:param str filename: the filename to save to
- :raises IOError: if the file could not be written
+ :raises: ``IOError`` -- if the file could not be written
"""
# update local host keys from file (in case other SSH clients
@@ -170,16 +170,10 @@ class SSHClient (ClosingContextManager):
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 **policy** is a "policy class" (or instance thereof), namely some
+ subclass of `.MissingHostKeyPolicy` such as `.RejectPolicy` (the
+ default), `.AutoAddPolicy`, `.WarningPolicy`, or a user-created
+ subclass.
* 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`).
@@ -188,6 +182,8 @@ class SSHClient (ClosingContextManager):
the policy to use when receiving a host key from a
previously-unknown server
"""
+ if inspect.isclass(policy):
+ policy = policy()
self._policy = policy
def _families_and_addresses(self, hostname, port):
@@ -230,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
@@ -282,11 +279,15 @@ 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
+ :raises:
+ `.BadHostKeyException` -- if the server's host key could not be
verified
- :raises AuthenticationException: if authentication failed
- :raises SSHException: if there was any other error connecting or
+ :raises: `.AuthenticationException` -- if authentication failed
+ :raises:
+ `.SSHException` -- if there was any other error connecting or
establishing an SSH session
:raises socket.error: if a socket error occurred while connecting
@@ -340,37 +341,43 @@ class SSHClient (ClosingContextManager):
t.set_log_channel(self._log_channel)
if banner_timeout is not None:
t.banner_timeout = banner_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()
+ if auth_timeout is not None:
+ t.auth_timeout = auth_timeout
if port == SSH_PORT:
server_hostkey_name = hostname
else:
server_hostkey_name = "[%s]:%d" % (hostname, port)
+ our_server_keys = None
# If GSS-API Key Exchange is performed we are not required to check the
# host key, because the host is authenticated via GSS-API / SSPI as
# well as our client.
if not self._transport.use_gss_kex:
- our_server_key = self._system_host_keys.get(
- server_hostkey_name, {}).get(keytype)
- if our_server_key is None:
- our_server_key = self._host_keys.get(server_hostkey_name,
- {}).get(keytype, None)
- if our_server_key is None:
- # will raise exception if the key is rejected;
- # let that fall out
- self._policy.missing_host_key(self, server_hostkey_name,
- server_key)
- # if the callback returns, assume the key is ok
- our_server_key = server_key
-
- if server_key != our_server_key:
- raise BadHostKeyException(hostname, server_key, our_server_key)
+ our_server_keys = self._system_host_keys.get(server_hostkey_name)
+ if our_server_keys is None:
+ our_server_keys = self._host_keys.get(server_hostkey_name)
+ if our_server_keys is not None:
+ keytype = our_server_keys.keys()[0]
+ sec_opts = t.get_security_options()
+ other_types = [x for x in sec_opts.key_types if x != keytype]
+ sec_opts.key_types = [keytype] + other_types
+
+ t.start_client(timeout=timeout)
+
+ if not self._transport.use_gss_kex:
+ server_key = t.get_remote_server_key()
+ if our_server_keys is None:
+ # will raise exception if the key is rejected
+ self._policy.missing_host_key(
+ self, server_hostkey_name, server_key
+ )
+ else:
+ our_key = our_server_keys.get(server_key.get_name())
+ if our_key != server_key:
+ if our_key is None:
+ our_key = list(our_server_keys.values())[0]
+ raise BadHostKeyException(hostname, server_key, our_key)
if username is None:
username = getpass.getuser()
@@ -424,7 +431,7 @@ class SSHClient (ClosingContextManager):
interpreted the same way as by the built-in ``file()`` function in
Python
:param int timeout:
- set command's channel timeout. See `Channel.settimeout`.settimeout
+ set command's channel timeout. See `.Channel.settimeout`
:param dict environment:
a dict of shell environment variables, to be merged into the
default environment that the remote command executes within.
@@ -437,7 +444,7 @@ class SSHClient (ClosingContextManager):
the stdin, stdout, and stderr of the executing command, as a
3-tuple
- :raises SSHException: if the server fails to execute the command
+ :raises: `.SSHException` -- if the server fails to execute the command
"""
chan = self._transport.open_session(timeout=timeout)
if get_pty:
@@ -467,7 +474,7 @@ class SSHClient (ClosingContextManager):
:param dict environment: the command's environment
:return: a new `.Channel` connected to the remote shell
- :raises SSHException: if the server fails to invoke a shell
+ :raises: `.SSHException` -- if the server fails to invoke a shell
"""
chan = self._transport.open_session()
chan.get_pty(term, width, height, width_pixels, height_pixels)
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 ae7f9799..99734458 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'
@@ -205,7 +199,7 @@ class DSSKey(PKey):
generate a new host key or authentication key.
:param int bits: number of bits the generated key should be.
- :param function progress_func: Unused
+ :param progress_func: Unused
:return: new `.DSSKey` private key
"""
numbers = dsa.generate_private_key(
diff --git a/paramiko/ecdsakey.py b/paramiko/ecdsakey.py
index b13b9a3c..1add88bd 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
@@ -227,7 +225,7 @@ class ECDSAKey(PKey):
Generate a new private ECDSA key. This factory function can be used to
generate a new host key or authentication key.
- :param function progress_func: Not used for this type of key.
+ :param progress_func: Not used for this type of key.
:returns: A new private key (`.ECDSAKey`) object
"""
if bits is not None:
diff --git a/paramiko/ed25519key.py b/paramiko/ed25519key.py
index e1a8a732..a50d68bc 100644
--- a/paramiko/ed25519key.py
+++ b/paramiko/ed25519key.py
@@ -167,6 +167,13 @@ class Ed25519Key(PKey):
m.add_string(v.encode())
return m.asbytes()
+ def __hash__(self):
+ if self.can_sign():
+ v = self._signing_key.verify_key
+ else:
+ v = self._verifying_key
+ return hash((self.get_name(), v))
+
def get_name(self):
return "ssh-ed25519"
diff --git a/paramiko/file.py b/paramiko/file.py
index e31ad9dd..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
@@ -67,7 +67,7 @@ class BufferedFile (ClosingContextManager):
file. This iterator happens to return the file itself, since a file is
its own iterator.
- :raises ValueError: if the file is closed.
+ :raises: ``ValueError`` -- if the file is closed.
"""
if self._closed:
raise ValueError('I/O operation on closed file')
@@ -93,10 +93,10 @@ class BufferedFile (ClosingContextManager):
def next(self):
"""
Returns the next line from the input, or raises
- `~exceptions.StopIteration` when EOF is hit. Unlike Python file
+ ``StopIteration`` when EOF is hit. Unlike Python file
objects, it's okay to mix calls to `next` and `readline`.
- :raises StopIteration: when the end of the file is reached.
+ :raises: ``StopIteration`` -- when the end of the file is reached.
:returns: a line (`str`) read from the file.
"""
@@ -107,11 +107,11 @@ class BufferedFile (ClosingContextManager):
else:
def __next__(self):
"""
- Returns the next line from the input, or raises `.StopIteration`
+ Returns the next line from the input, or raises ``StopIteration``
when EOF is hit. Unlike python file objects, it's okay to mix
calls to `.next` and `.readline`.
- :raises StopIteration: when the end of the file is reached.
+ :raises: ``StopIteration`` -- when the end of the file is reached.
:returns: a line (`str`) read from the file.
"""
@@ -152,8 +152,8 @@ class BufferedFile (ClosingContextManager):
def readinto(self, buff):
"""
- Read up to ``len(buff)`` bytes into :class:`bytearray` *buff* and
- return the number of bytes read.
+ Read up to ``len(buff)`` bytes into ``bytearray`` *buff* and return the
+ number of bytes read.
:returns:
The number of bytes read.
@@ -368,7 +368,7 @@ class BufferedFile (ClosingContextManager):
type of movement: 0 = absolute; 1 = relative to the current
position; 2 = relative to the end of the file.
- :raises IOError: if the file doesn't support random access.
+ :raises: ``IOError`` -- if the file doesn't support random access.
"""
raise IOError('File does not support seeking.')
@@ -389,9 +389,11 @@ class BufferedFile (ClosingContextManager):
written yet. (Use `flush` or `close` to force buffered data to be
written out.)
- :param str/bytes data: data to write
+ :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):
@@ -423,7 +425,7 @@ class BufferedFile (ClosingContextManager):
name is intended to match `readlines`; `writelines` does not add line
separators.)
- :param iterable sequence: an iterable sequence of strings.
+ :param sequence: an iterable sequence of strings.
"""
for line in sequence:
self.write(line)
diff --git a/paramiko/hostkeys.py b/paramiko/hostkeys.py
index f3cb29db..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
@@ -91,7 +86,7 @@ class HostKeys (MutableMapping):
:param str filename: name of the file to read host keys from
- :raises IOError: if there was an error reading the file
+ :raises: ``IOError`` -- if there was an error reading the file
"""
with open(filename, 'r') as f:
for lineno, line in enumerate(f, 1):
@@ -119,7 +114,7 @@ class HostKeys (MutableMapping):
:param str filename: name of the file to write
- :raises IOError: if there was an error writing the file
+ :raises: ``IOError`` -- if there was an error writing the file
.. versionadded:: 1.6.1
"""
diff --git a/paramiko/kex_ecdh_nist.py b/paramiko/kex_ecdh_nist.py
new file mode 100644
index 00000000..702a872d
--- /dev/null
+++ b/paramiko/kex_ecdh_nist.py
@@ -0,0 +1,118 @@
+"""
+Ephemeral Elliptic Curve Diffie-Hellman (ECDH) key exchange
+RFC 5656, Section 4
+"""
+
+from hashlib import sha256, sha384, sha512
+from paramiko.message import Message
+from paramiko.py3compat import byte_chr, long
+from paramiko.ssh_exception import SSHException
+from cryptography.hazmat.backends import default_backend
+from cryptography.hazmat.primitives.asymmetric import ec
+from binascii import hexlify
+
+_MSG_KEXECDH_INIT, _MSG_KEXECDH_REPLY = range(30, 32)
+c_MSG_KEXECDH_INIT, c_MSG_KEXECDH_REPLY = [byte_chr(c) for c in range(30, 32)]
+
+
+class KexNistp256():
+
+ name = "ecdh-sha2-nistp256"
+ hash_algo = sha256
+ curve = ec.SECP256R1()
+
+ def __init__(self, transport):
+ self.transport = transport
+ # private key, client public and server public keys
+ self.P = long(0)
+ self.Q_C = None
+ self.Q_S = None
+
+ def start_kex(self):
+ self._generate_key_pair()
+ if self.transport.server_mode:
+ self.transport._expect_packet(_MSG_KEXECDH_INIT)
+ return
+ m = Message()
+ m.add_byte(c_MSG_KEXECDH_INIT)
+ # SEC1: V2.0 2.3.3 Elliptic-Curve-Point-to-Octet-String Conversion
+ m.add_string(self.Q_C.public_numbers().encode_point())
+ self.transport._send_message(m)
+ self.transport._expect_packet(_MSG_KEXECDH_REPLY)
+
+ def parse_next(self, ptype, m):
+ if self.transport.server_mode and (ptype == _MSG_KEXECDH_INIT):
+ return self._parse_kexecdh_init(m)
+ elif not self.transport.server_mode and (ptype == _MSG_KEXECDH_REPLY):
+ return self._parse_kexecdh_reply(m)
+ raise SSHException('KexECDH asked to handle packet type %d' % ptype)
+
+ def _generate_key_pair(self):
+ self.P = ec.generate_private_key(self.curve, default_backend())
+ if self.transport.server_mode:
+ self.Q_S = self.P.public_key()
+ return
+ self.Q_C = self.P.public_key()
+
+ def _parse_kexecdh_init(self, m):
+ Q_C_bytes = m.get_string()
+ self.Q_C = ec.EllipticCurvePublicNumbers.from_encoded_point(
+ self.curve, Q_C_bytes
+ )
+ K_S = self.transport.get_server_key().asbytes()
+ K = self.P.exchange(ec.ECDH(), self.Q_C.public_key(default_backend()))
+ K = long(hexlify(K), 16)
+ # compute exchange hash
+ hm = Message()
+ hm.add(self.transport.remote_version, self.transport.local_version,
+ self.transport.remote_kex_init, self.transport.local_kex_init)
+ hm.add_string(K_S)
+ hm.add_string(Q_C_bytes)
+ # SEC1: V2.0 2.3.3 Elliptic-Curve-Point-to-Octet-String Conversion
+ hm.add_string(self.Q_S.public_numbers().encode_point())
+ hm.add_mpint(long(K))
+ H = self.hash_algo(hm.asbytes()).digest()
+ self.transport._set_K_H(K, H)
+ sig = self.transport.get_server_key().sign_ssh_data(H)
+ # construct reply
+ m = Message()
+ m.add_byte(c_MSG_KEXECDH_REPLY)
+ m.add_string(K_S)
+ m.add_string(self.Q_S.public_numbers().encode_point())
+ m.add_string(sig)
+ self.transport._send_message(m)
+ self.transport._activate_outbound()
+
+ def _parse_kexecdh_reply(self, m):
+ K_S = m.get_string()
+ Q_S_bytes = m.get_string()
+ self.Q_S = ec.EllipticCurvePublicNumbers.from_encoded_point(
+ self.curve, Q_S_bytes
+ )
+ sig = m.get_binary()
+ K = self.P.exchange(ec.ECDH(), self.Q_S.public_key(default_backend()))
+ K = long(hexlify(K), 16)
+ # compute exchange hash and verify signature
+ hm = Message()
+ hm.add(self.transport.local_version, self.transport.remote_version,
+ self.transport.local_kex_init, self.transport.remote_kex_init)
+ hm.add_string(K_S)
+ # SEC1: V2.0 2.3.3 Elliptic-Curve-Point-to-Octet-String Conversion
+ hm.add_string(self.Q_C.public_numbers().encode_point())
+ hm.add_string(Q_S_bytes)
+ hm.add_mpint(K)
+ self.transport._set_K_H(K, self.hash_algo(hm.asbytes()).digest())
+ self.transport._verify_key(K_S, sig)
+ self.transport._activate_outbound()
+
+
+class KexNistp384(KexNistp256):
+ name = "ecdh-sha2-nistp384"
+ hash_algo = sha384
+ curve = ec.SECP384R1()
+
+
+class KexNistp521(KexNistp256):
+ name = "ecdh-sha2-nistp521"
+ hash_algo = sha512
+ curve = ec.SECP521R1()
diff --git a/paramiko/kex_gss.py b/paramiko/kex_gss.py
index 40ceb5cd..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
@@ -108,7 +108,7 @@ class KexGSSGroup1(object):
"""
Parse the next packet.
- :param char ptype: The type of the incoming packet
+ :param ptype: The (string) type of the incoming packet
:param `.Message` m: The paket content
"""
if self.transport.server_mode and (ptype == MSG_KEXGSS_INIT):
@@ -345,7 +345,7 @@ class KexGSSGex(object):
"""
Parse the next packet.
- :param char ptype: The type of the incoming packet
+ :param ptype: The (string) type of the incoming packet
:param `.Message` m: The paket content
"""
if ptype == MSG_KEXGSS_GROUPREQ:
diff --git a/paramiko/packet.py b/paramiko/packet.py
index 16288a0a..95a26c6e 100644
--- a/paramiko/packet.py
+++ b/paramiko/packet.py
@@ -43,6 +43,9 @@ def compute_hmac(key, message, digest_class):
class NeedRekeyException (Exception):
+ """
+ Exception indicating a rekey is needed.
+ """
pass
@@ -253,8 +256,9 @@ class Packetizer (object):
:param int n: number of bytes to read
:return: the data read, as a `str`
- :raises EOFError:
- if the socket was closed before all the bytes could be read
+ :raises:
+ ``EOFError`` -- if the socket was closed before all the bytes could
+ be read
"""
out = bytes()
# handle over-reading from reading the banner line
@@ -413,8 +417,8 @@ class Packetizer (object):
Only one thread should ever be in this function (no other locking is
done).
- :raises SSHException: if the packet is mangled
- :raises NeedRekeyException: if the transport should rekey
+ :raises: `.SSHException` -- if the packet is mangled
+ :raises: `.NeedRekeyException` -- if the transport should rekey
"""
header = self.read_all(self.__block_size_in, check_rekey=True)
if self.__block_engine_in is not None:
diff --git a/paramiko/pkey.py b/paramiko/pkey.py
index af9370fc..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,
@@ -68,7 +74,7 @@ class PKey(object):
:param str data: an optional string containing a public key
of this type
- :raises SSHException:
+ :raises: `.SSHException` --
if a key cannot be created from the ``data`` or ``msg`` given, or
no key was passed in.
"""
@@ -95,7 +101,7 @@ class PKey(object):
of the key are compared, so a public key will compare equal to its
corresponding private key.
- :param .Pkey other: key to compare to.
+ :param .PKey other: key to compare to.
"""
hs = hash(self)
ho = hash(other)
@@ -191,10 +197,10 @@ class PKey(object):
encrypted
:return: a new `.PKey` based on the given private key
- :raises IOError: if there was an error reading the file
- :raises PasswordRequiredException: if the private key file is
+ :raises: ``IOError`` -- if there was an error reading the file
+ :raises: `.PasswordRequiredException` -- if the private key file is
encrypted, and ``password`` is ``None``
- :raises SSHException: if the key file is invalid
+ :raises: `.SSHException` -- if the key file is invalid
"""
key = cls(filename=filename, password=password)
return key
@@ -212,10 +218,10 @@ class PKey(object):
an optional password to use to decrypt the key, if it's encrypted
:return: a new `.PKey` based on the given private key
- :raises IOError: if there was an error reading the key
- :raises PasswordRequiredException:
+ :raises: ``IOError`` -- if there was an error reading the key
+ :raises: `.PasswordRequiredException` --
if the private key file is encrypted, and ``password`` is ``None``
- :raises SSHException: if the key file is invalid
+ :raises: `.SSHException` -- if the key file is invalid
"""
key = cls(file_obj=file_obj, password=password)
return key
@@ -229,8 +235,8 @@ class PKey(object):
:param str password:
an optional password to use to encrypt the key file
- :raises IOError: if there was an error writing the file
- :raises SSHException: if the key is invalid
+ :raises: ``IOError`` -- if there was an error writing the file
+ :raises: `.SSHException` -- if the key is invalid
"""
raise Exception('Not implemented in PKey')
@@ -242,8 +248,8 @@ class PKey(object):
:param file_obj: the file-like object to write into
:param str password: an optional password to use to encrypt the key
- :raises IOError: if there was an error writing to the file
- :raises SSHException: if the key is invalid
+ :raises: ``IOError`` -- if there was an error writing to the file
+ :raises: `.SSHException` -- if the key is invalid
"""
raise Exception('Not implemented in PKey')
@@ -263,10 +269,10 @@ class PKey(object):
encrypted.
:return: data blob (`str`) that makes up the private key.
- :raises IOError: if there was an error reading the file.
- :raises PasswordRequiredException: if the private key file is
+ :raises: ``IOError`` -- if there was an error reading the file.
+ :raises: `.PasswordRequiredException` -- if the private key file is
encrypted, and ``password`` is ``None``.
- :raises SSHException: if the key file is invalid.
+ :raises: `.SSHException` -- if the key file is invalid.
"""
with open(filename, 'r') as f:
data = self._read_private_key(tag, f, password)
@@ -340,17 +346,17 @@ class PKey(object):
:param str data: data blob that makes up the private key.
:param str password: an optional password to use to encrypt the file.
- :raises IOError: if there was an error writing the file.
+ :raises: ``IOError`` -- if there was an error writing the file.
"""
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 8953a626..a457a121 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'
@@ -155,7 +153,7 @@ class RSAKey(PKey):
generate a new host key or authentication key.
:param int bits: number of bits the generated key should be.
- :param function progress_func: Unused
+ :param progress_func: Unused
:return: new `.RSAKey` private key
"""
key = rsa.generate_private_key(
diff --git a/paramiko/server.py b/paramiko/server.py
index b2a07916..adc606bf 100644
--- a/paramiko/server.py
+++ b/paramiko/server.py
@@ -106,15 +106,15 @@ class ServerInterface (object):
Determine if a client may open channels with no (further)
authentication.
- Return `.AUTH_FAILED` if the client must authenticate, or
- `.AUTH_SUCCESSFUL` if it's okay for the client to not
+ Return ``AUTH_FAILED`` if the client must authenticate, or
+ ``AUTH_SUCCESSFUL`` if it's okay for the client to not
authenticate.
- The default implementation always returns `.AUTH_FAILED`.
+ The default implementation always returns ``AUTH_FAILED``.
:param str username: the username of the client.
:return:
- `.AUTH_FAILED` if the authentication fails; `.AUTH_SUCCESSFUL` if
+ ``AUTH_FAILED`` if the authentication fails; ``AUTH_SUCCESSFUL`` if
it succeeds.
:rtype: int
"""
@@ -125,21 +125,21 @@ class ServerInterface (object):
Determine if a given username and password supplied by the client is
acceptable for use in authentication.
- Return `.AUTH_FAILED` if the password is not accepted,
- `.AUTH_SUCCESSFUL` if the password is accepted and completes
- the authentication, or `.AUTH_PARTIALLY_SUCCESSFUL` if your
+ Return ``AUTH_FAILED`` if the password is not accepted,
+ ``AUTH_SUCCESSFUL`` if the password is accepted and completes
+ the authentication, or ``AUTH_PARTIALLY_SUCCESSFUL`` if your
authentication is stateful, and this key is accepted for
authentication, but more authentication is required. (In this latter
case, `get_allowed_auths` will be called to report to the client what
options it has for continuing the authentication.)
- The default implementation always returns `.AUTH_FAILED`.
+ The default implementation always returns ``AUTH_FAILED``.
:param str username: the username of the authenticating client.
:param str password: the password given by the client.
:return:
- `.AUTH_FAILED` if the authentication fails; `.AUTH_SUCCESSFUL` if
- it succeeds; `.AUTH_PARTIALLY_SUCCESSFUL` if the password auth is
+ ``AUTH_FAILED`` if the authentication fails; ``AUTH_SUCCESSFUL`` if
+ it succeeds; ``AUTH_PARTIALLY_SUCCESSFUL`` if the password auth is
successful, but authentication must continue.
:rtype: int
"""
@@ -152,9 +152,9 @@ class ServerInterface (object):
check the username and key and decide if you would accept a signature
made using this key.
- Return `.AUTH_FAILED` if the key is not accepted,
- `.AUTH_SUCCESSFUL` if the key is accepted and completes the
- authentication, or `.AUTH_PARTIALLY_SUCCESSFUL` if your
+ Return ``AUTH_FAILED`` if the key is not accepted,
+ ``AUTH_SUCCESSFUL`` if the key is accepted and completes the
+ authentication, or ``AUTH_PARTIALLY_SUCCESSFUL`` if your
authentication is stateful, and this password is accepted for
authentication, but more authentication is required. (In this latter
case, `get_allowed_auths` will be called to report to the client what
@@ -164,13 +164,13 @@ class ServerInterface (object):
If you're willing to accept the key, Paramiko will do the work of
verifying the client's signature.
- The default implementation always returns `.AUTH_FAILED`.
+ The default implementation always returns ``AUTH_FAILED``.
:param str username: the username of the authenticating client
:param .PKey key: the key object provided by the client
:return:
- `.AUTH_FAILED` if the client can't authenticate with this key;
- `.AUTH_SUCCESSFUL` if it can; `.AUTH_PARTIALLY_SUCCESSFUL` if it
+ ``AUTH_FAILED`` if the client can't authenticate with this key;
+ ``AUTH_SUCCESSFUL`` if it can; ``AUTH_PARTIALLY_SUCCESSFUL`` if it
can authenticate with this key but must continue with
authentication
:rtype: int
@@ -184,19 +184,19 @@ class ServerInterface (object):
``"keyboard-interactive"`` auth type, which requires you to send a
series of questions for the client to answer.
- Return `.AUTH_FAILED` if this auth method isn't supported. Otherwise,
+ Return ``AUTH_FAILED`` if this auth method isn't supported. Otherwise,
you should return an `.InteractiveQuery` object containing the prompts
and instructions for the user. The response will be sent via a call
to `check_auth_interactive_response`.
- The default implementation always returns `.AUTH_FAILED`.
+ The default implementation always returns ``AUTH_FAILED``.
:param str username: the username of the authenticating client
:param str submethods:
a comma-separated list of methods preferred by the client (usually
empty)
:return:
- `.AUTH_FAILED` if this auth method isn't supported; otherwise an
+ ``AUTH_FAILED`` if this auth method isn't supported; otherwise an
object containing queries for the user
:rtype: int or `.InteractiveQuery`
"""
@@ -208,9 +208,9 @@ class ServerInterface (object):
supported. You should override this method in server mode if you want
to support the ``"keyboard-interactive"`` auth type.
- Return `.AUTH_FAILED` if the responses are not accepted,
- `.AUTH_SUCCESSFUL` if the responses are accepted and complete
- the authentication, or `.AUTH_PARTIALLY_SUCCESSFUL` if your
+ Return ``AUTH_FAILED`` if the responses are not accepted,
+ ``AUTH_SUCCESSFUL`` if the responses are accepted and complete
+ the authentication, or ``AUTH_PARTIALLY_SUCCESSFUL`` if your
authentication is stateful, and this set of responses is accepted for
authentication, but more authentication is required. (In this latter
case, `get_allowed_auths` will be called to report to the client what
@@ -221,12 +221,12 @@ class ServerInterface (object):
client to respond with more answers, calling this method again. This
cycle can continue indefinitely.
- The default implementation always returns `.AUTH_FAILED`.
+ The default implementation always returns ``AUTH_FAILED``.
:param list responses: list of `str` responses from the client
:return:
- `.AUTH_FAILED` if the authentication fails; `.AUTH_SUCCESSFUL` if
- it succeeds; `.AUTH_PARTIALLY_SUCCESSFUL` if the interactive auth
+ ``AUTH_FAILED`` if the authentication fails; ``AUTH_SUCCESSFUL`` if
+ it succeeds; ``AUTH_PARTIALLY_SUCCESSFUL`` if the interactive auth
is successful, but authentication must continue; otherwise an
object containing queries for the user
:rtype: int or `.InteractiveQuery`
@@ -243,8 +243,8 @@ class ServerInterface (object):
:param str username: The username of the authenticating client
:param int gss_authenticated: The result of the krb5 authentication
:param str cc_filename: The krb5 client credentials cache filename
- :return: `.AUTH_FAILED` if the user is not authenticated otherwise
- `.AUTH_SUCCESSFUL`
+ :return: ``AUTH_FAILED`` if the user is not authenticated otherwise
+ ``AUTH_SUCCESSFUL``
:rtype: int
:note: Kerberos credential delegation is not supported.
:see: `.ssh_gss`
@@ -257,7 +257,7 @@ class ServerInterface (object):
your local kerberos library to make sure that the
krb5_principal has an account on the server and is allowed to
log in as a user.
- :see: `http://www.unix.com/man-page/all/3/krb5_kuserok/`
+ :see: http://www.unix.com/man-page/all/3/krb5_kuserok/
"""
if gss_authenticated == AUTH_SUCCESSFUL:
return AUTH_SUCCESSFUL
@@ -275,8 +275,8 @@ class ServerInterface (object):
:param str username: The username of the authenticating client
:param int gss_authenticated: The result of the krb5 authentication
:param str cc_filename: The krb5 client credentials cache filename
- :return: `.AUTH_FAILED` if the user is not authenticated otherwise
- `.AUTH_SUCCESSFUL`
+ :return: ``AUTH_FAILED`` if the user is not authenticated otherwise
+ ``AUTH_SUCCESSFUL``
:rtype: int
:note: Kerberos credential delegation is not supported.
:see: `.ssh_gss` `.kex_gss`
@@ -289,7 +289,7 @@ class ServerInterface (object):
your local kerberos library to make sure that the
krb5_principal has an account on the server and is allowed
to log in as a user.
- :see: `http://www.unix.com/man-page/all/3/krb5_kuserok/`
+ :see: http://www.unix.com/man-page/all/3/krb5_kuserok/
"""
if gss_authenticated == AUTH_SUCCESSFUL:
return AUTH_SUCCESSFUL
@@ -301,9 +301,8 @@ class ServerInterface (object):
authentication.
The default implementation always returns false.
- :return: True if GSSAPI authentication is enabled otherwise false
- :rtype: Boolean
- :see: : `.ssh_gss`
+ :returns bool: Whether GSSAPI authentication is enabled.
+ :see: `.ssh_gss`
"""
UseGSSAPI = False
return UseGSSAPI
diff --git a/paramiko/sftp_client.py b/paramiko/sftp_client.py
index ea6f88a9..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
@@ -83,8 +81,8 @@ class SFTPClient(BaseSFTP, ClosingContextManager):
:param .Channel sock: an open `.Channel` using the ``"sftp"`` subsystem
- :raises SSHException: if there's an exception while negotiating
- sftp
+ :raises:
+ `.SSHException` -- if there's an exception while negotiating sftp
"""
BaseSFTP.__init__(self)
self.sock = sock
@@ -321,7 +319,7 @@ class SFTPClient(BaseSFTP, ClosingContextManager):
:param int bufsize: desired buffering (-1 = default buffer size)
:return: an `.SFTPFile` object representing the open file
- :raises IOError: if the file could not be opened.
+ :raises: ``IOError`` -- if the file could not be opened.
"""
filename = self._adjust_cwd(filename)
self._log(DEBUG, 'open(%r, %r)' % (filename, mode))
@@ -356,7 +354,7 @@ class SFTPClient(BaseSFTP, ClosingContextManager):
:param str path: path (absolute or relative) of the file to remove
- :raises IOError: if the path refers to a folder (directory)
+ :raises: ``IOError`` -- if the path refers to a folder (directory)
"""
path = self._adjust_cwd(path)
self._log(DEBUG, 'remove(%r)' % path)
@@ -368,10 +366,13 @@ 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
+ :raises:
+ ``IOError`` -- if ``newpath`` is a folder, or something else goes
wrong
"""
oldpath = self._adjust_cwd(oldpath)
@@ -379,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``.
@@ -450,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
@@ -522,8 +542,7 @@ class SFTPClient(BaseSFTP, ClosingContextManager):
method on Python file objects.
:param str path: path of the file to modify
- :param size: the new size of the file
- :type size: int or long
+ :param int size: the new size of the file
"""
path = self._adjust_cwd(path)
self._log(DEBUG, 'truncate(%r, %r)' % (path, size))
@@ -562,7 +581,7 @@ class SFTPClient(BaseSFTP, ClosingContextManager):
:param str path: path to be normalized
:return: normalized form of the given path (as a `str`)
- :raises IOError: if the path can't be resolved on the server
+ :raises: ``IOError`` -- if the path can't be resolved on the server
"""
path = self._adjust_cwd(path)
self._log(DEBUG, 'normalize(%r)' % path)
@@ -585,7 +604,8 @@ class SFTPClient(BaseSFTP, ClosingContextManager):
:param str path: new current working directory
- :raises IOError: if the requested path doesn't exist on the server
+ :raises:
+ ``IOError`` -- if the requested path doesn't exist on the server
.. versionadded:: 1.4
"""
@@ -757,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_file.py b/paramiko/sftp_file.py
index 58653c79..337cdbeb 100644
--- a/paramiko/sftp_file.py
+++ b/paramiko/sftp_file.py
@@ -251,6 +251,11 @@ class SFTPFile (BufferedFile):
return True
def seek(self, offset, whence=0):
+ """
+ Set the file's current position.
+
+ See `file.seek` for details.
+ """
self.flush()
if whence == self.SEEK_SET:
self._realpos = self._pos = offset
@@ -267,8 +272,8 @@ class SFTPFile (BufferedFile):
exactly like `.SFTPClient.stat`, except that it operates on an
already-open file.
- :return: an `.SFTPAttributes` object containing attributes about this
- file.
+ :returns:
+ an `.SFTPAttributes` object containing attributes about this file.
"""
t, msg = self.sftp._request(CMD_FSTAT, self.handle)
if t != CMD_ATTRS:
@@ -332,7 +337,6 @@ class SFTPFile (BufferedFile):
Python file objects.
:param size: the new size of the file
- :type size: int or long
"""
self.sftp._log(
DEBUG,
@@ -370,21 +374,18 @@ class SFTPFile (BufferedFile):
:param offset:
offset into the file to begin hashing (0 means to start from the
beginning)
- :type offset: int or long
:param length:
number of bytes to hash (0 means continue to the end of the file)
- :type length: int or long
:param int block_size:
number of bytes to hash per result (must not be less than 256; 0
means to compute only one hash of the entire segment)
- :type block_size: int
:return:
`str` of bytes representing the hash of each block, concatenated
together
- :raises IOError: if the server doesn't support the "check-file"
- extension, or possibly doesn't support the hash algorithm
- requested
+ :raises:
+ ``IOError`` -- if the server doesn't support the "check-file"
+ extension, or possibly doesn't support the hash algorithm requested
.. note:: Many (most?) servers don't support this extension yet.
@@ -466,9 +467,8 @@ class SFTPFile (BufferedFile):
once.
:param chunks:
- a list of (offset, length) tuples indicating which sections of the
- file to read
- :type chunks: list(tuple(long, int))
+ a list of ``(offset, length)`` tuples indicating which sections of
+ the file to read
:return: a list of blocks read, in the same order as in ``chunks``
.. versionadded:: 1.5.4
diff --git a/paramiko/sftp_handle.py b/paramiko/sftp_handle.py
index 2d2e621c..ca473900 100644
--- a/paramiko/sftp_handle.py
+++ b/paramiko/sftp_handle.py
@@ -77,7 +77,7 @@ class SFTPHandle (ClosingContextManager):
to be 64 bits.
If the end of the file has been reached, this method may return an
- empty string to signify EOF, or it may also return `.SFTP_EOF`.
+ empty string to signify EOF, or it may also return ``SFTP_EOF``.
The default implementation checks for an attribute on ``self`` named
``readfile``, and if present, performs the read operation on the Python
@@ -85,7 +85,6 @@ class SFTPHandle (ClosingContextManager):
common case where you are wrapping a Python file object.)
:param offset: position in the file to start reading from.
- :type offset: int or long
:param int length: number of bytes to attempt to read.
:return: data read from the file, or an SFTP error code, as a `str`.
"""
@@ -120,9 +119,8 @@ class SFTPHandle (ClosingContextManager):
refer to the same file.
:param offset: position in the file to start reading from.
- :type offset: int or long
:param str data: data to write into the file.
- :return: an SFTP error code like `.SFTP_OK`.
+ :return: an SFTP error code like ``SFTP_OK``.
"""
writefile = getattr(self, 'writefile', None)
if writefile is None:
@@ -152,7 +150,7 @@ class SFTPHandle (ClosingContextManager):
:return:
an attributes object for the given file, or an SFTP error code
- (like `.SFTP_PERMISSION_DENIED`).
+ (like ``SFTP_PERMISSION_DENIED``).
:rtype: `.SFTPAttributes` or error code
"""
return SFTP_OP_UNSUPPORTED
@@ -164,7 +162,7 @@ class SFTPHandle (ClosingContextManager):
check for the presence of fields before using them.
:param .SFTPAttributes attr: the attributes to change on this file.
- :return: an `int` error code like `.SFTP_OK`.
+ :return: an `int` error code like ``SFTP_OK``.
"""
return SFTP_OP_UNSUPPORTED
diff --git a/paramiko/sftp_server.py b/paramiko/sftp_server.py
index f94c5e39..f7d1c657 100644
--- a/paramiko/sftp_server.py
+++ b/paramiko/sftp_server.py
@@ -72,7 +72,7 @@ class SFTPServer (BaseSFTP, SubsystemHandler):
:param str name: name of the requested subsystem.
:param .ServerInterface server:
the server object associated with this channel and subsystem
- :param class sftp_si:
+ :param sftp_si:
a subclass of `.SFTPServerInterface` to use for handling individual
requests.
"""
@@ -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 c335eaec..40969309 100644
--- a/paramiko/sftp_si.py
+++ b/paramiko/sftp_si.py
@@ -72,7 +72,7 @@ class SFTPServerInterface (object):
on that file. On success, a new object subclassed from `.SFTPHandle`
should be returned. This handle will be used for future operations
on the file (read, write, etc). On failure, an error code such as
- `.SFTP_PERMISSION_DENIED` should be returned.
+ ``SFTP_PERMISSION_DENIED`` should be returned.
``flags`` contains the requested mode for opening (read-only,
write-append, etc) as a bitset of flags from the ``os`` module:
@@ -120,7 +120,7 @@ class SFTPServerInterface (object):
`.SFTPAttributes.from_stat` will usually do what you want.
In case of an error, you should return one of the ``SFTP_*`` error
- codes, such as `.SFTP_PERMISSION_DENIED`.
+ codes, such as ``SFTP_PERMISSION_DENIED``.
:param str path: the requested path (relative or absolute) to be
listed.
@@ -150,7 +150,7 @@ class SFTPServerInterface (object):
for.
:return:
an `.SFTPAttributes` object for the given file, or an SFTP error
- code (like `.SFTP_PERMISSION_DENIED`).
+ code (like ``SFTP_PERMISSION_DENIED``).
"""
return SFTP_OP_UNSUPPORTED
@@ -168,7 +168,7 @@ class SFTPServerInterface (object):
:type path: str
:return:
an `.SFTPAttributes` object for the given file, or an SFTP error
- code (like `.SFTP_PERMISSION_DENIED`).
+ code (like ``SFTP_PERMISSION_DENIED``).
"""
return SFTP_OP_UNSUPPORTED
@@ -178,7 +178,7 @@ class SFTPServerInterface (object):
:param str path:
the requested path (relative or absolute) of the file to delete.
- :return: an SFTP error code `int` like `.SFTP_OK`.
+ :return: an SFTP error code `int` like ``SFTP_OK``.
"""
return SFTP_OP_UNSUPPORTED
@@ -197,7 +197,19 @@ class SFTPServerInterface (object):
: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: an SFTP error code `int` like ``SFTP_OK``.
+ """
+ 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
@@ -214,7 +226,7 @@ class SFTPServerInterface (object):
:param str path:
requested path (relative or absolute) of the new folder.
:param .SFTPAttributes attr: requested attributes of the new folder.
- :return: an SFTP error code `int` like `.SFTP_OK`.
+ :return: an SFTP error code `int` like ``SFTP_OK``.
"""
return SFTP_OP_UNSUPPORTED
@@ -226,7 +238,7 @@ class SFTPServerInterface (object):
:param str path:
requested path (relative or absolute) of the folder to remove.
- :return: an SFTP error code `int` like `.SFTP_OK`.
+ :return: an SFTP error code `int` like ``SFTP_OK``.
"""
return SFTP_OP_UNSUPPORTED
@@ -241,7 +253,7 @@ class SFTPServerInterface (object):
:param attr:
requested attributes to change on the file (an `.SFTPAttributes`
object)
- :return: an error code `int` like `.SFTP_OK`.
+ :return: an error code `int` like ``SFTP_OK``.
"""
return SFTP_OP_UNSUPPORTED
@@ -277,7 +289,7 @@ class SFTPServerInterface (object):
:param str path: path (relative or absolute) of the symbolic link.
:return:
the target `str` path of the symbolic link, or an error code like
- `.SFTP_NO_SUCH_FILE`.
+ ``SFTP_NO_SUCH_FILE``.
"""
return SFTP_OP_UNSUPPORTED
diff --git a/paramiko/ssh_exception.py b/paramiko/ssh_exception.py
index 280a7f39..e9ab8d66 100644
--- a/paramiko/ssh_exception.py
+++ b/paramiko/ssh_exception.py
@@ -50,12 +50,10 @@ class BadAuthenticationType (AuthenticationException):
the server isn't allowing that type. (It may only allow public-key, for
example.)
- :ivar list allowed_types:
- list of allowed authentication types provided by the server (possible
- values are: ``"none"``, ``"password"``, and ``"publickey"``).
-
.. versionadded:: 1.1
"""
+ #: list of allowed authentication types provided by the server (possible
+ #: values are: ``"none"``, ``"password"``, and ``"publickey"``).
allowed_types = []
def __init__(self, explanation, types):
@@ -87,7 +85,7 @@ class ChannelException (SSHException):
"""
Exception raised when an attempt to open a new `.Channel` fails.
- :ivar int code: the error code returned by the server
+ :param int code: the error code returned by the server
.. versionadded:: 1.6
"""
@@ -102,9 +100,9 @@ class BadHostKeyException (SSHException):
"""
The host key given by the SSH server did not match what we were expecting.
- :ivar str hostname: the hostname of the SSH server
- :ivar PKey got_key: the host key presented by the server
- :ivar PKey expected_key: the host key expected
+ :param str hostname: the hostname of the SSH server
+ :param PKey got_key: the host key presented by the server
+ :param PKey expected_key: the host key expected
.. versionadded:: 1.6
"""
@@ -125,8 +123,8 @@ class ProxyCommandFailure (SSHException):
"""
The "ProxyCommand" found in the .ssh/config file returned an error.
- :ivar str command: The command line that is generating this exception.
- :ivar str error: The error captured from the proxy command output.
+ :param str command: The command line that is generating this exception.
+ :param str error: The error captured from the proxy command output.
"""
def __init__(self, command, error):
SSHException.__init__(self,
diff --git a/paramiko/ssh_gss.py b/paramiko/ssh_gss.py
index 9c88c6fc..414485f9 100644
--- a/paramiko/ssh_gss.py
+++ b/paramiko/ssh_gss.py
@@ -72,9 +72,8 @@ def GSSAuth(auth_method, gss_deleg_creds=True):
We delegate credentials by default.
:return: Either an `._SSH_GSSAPI` (Unix) object or an
`_SSH_SSPI` (Windows) object
- :rtype: Object
- :raise ImportError: If no GSS-API / SSPI module could be imported.
+ :raises: ``ImportError`` -- If no GSS-API / SSPI module could be imported.
:see: `RFC 4462 <http://www.ietf.org/rfc/rfc4462.txt>`_
:note: Check for the available API and return either an `._SSH_GSSAPI`
@@ -131,7 +130,6 @@ class _SSH_GSSAuth(object):
as the only service value.
:param str service: The desired SSH service
- :rtype: Void
"""
if service.find("ssh-"):
self._service = service
@@ -142,7 +140,6 @@ class _SSH_GSSAuth(object):
username is not set by C{ssh_init_sec_context}.
:param str username: The name of the user who attempts to login
- :rtype: Void
"""
self._username = username
@@ -155,7 +152,6 @@ class _SSH_GSSAuth(object):
:return: A byte sequence containing the number of supported
OIDs, the length of the OID and the actual OID encoded with
DER
- :rtype: Bytes
:note: In server mode we just return the OID length and the DER encoded
OID.
"""
@@ -172,7 +168,6 @@ class _SSH_GSSAuth(object):
:param str desired_mech: The desired GSS-API mechanism of the client
:return: ``True`` if the given OID is supported, otherwise C{False}
- :rtype: Boolean
"""
mech, __ = decoder.decode(desired_mech)
if mech.__str__() != self._krb5_mech:
@@ -187,7 +182,6 @@ class _SSH_GSSAuth(object):
:param int integer: The integer value to convert
:return: The byte sequence of an 32 bit integer
- :rtype: Bytes
"""
return struct.pack("!I", integer)
@@ -207,7 +201,6 @@ class _SSH_GSSAuth(object):
string service (ssh-connection),
string authentication-method
(gssapi-with-mic or gssapi-keyex)
- :rtype: Bytes
"""
mic = self._make_uint32(len(session_id))
mic += session_id
@@ -256,11 +249,11 @@ class _SSH_GSSAPI(_SSH_GSSAuth):
("pseudo negotiated" mechanism, because we
support just the krb5 mechanism :-))
:param str recv_token: The GSS-API token received from the Server
- :raise SSHException: Is raised if the desired mechanism of the client
- is not supported
+ :raises:
+ `.SSHException` -- Is raised if the desired mechanism of the client
+ is not supported
:return: A ``String`` if the GSS-API has returned a token or
``None`` if no token was returned
- :rtype: String or None
"""
self._username = username
self._gss_host = target
@@ -304,8 +297,6 @@ class _SSH_GSSAPI(_SSH_GSSAuth):
gssapi-keyex:
Returns the MIC token from GSS-API with the SSH session ID as
message.
- :rtype: String
- :see: `._ssh_build_mic`
"""
self._session_id = session_id
if not gss_kex:
@@ -329,7 +320,6 @@ class _SSH_GSSAPI(_SSH_GSSAuth):
if it's not the initial call.
:return: A ``String`` if the GSS-API has returned a token or ``None``
if no token was returned
- :rtype: String or None
"""
# hostname and username are not required for GSSAPI, but for SSPI
self._gss_host = hostname
@@ -348,7 +338,7 @@ class _SSH_GSSAPI(_SSH_GSSAuth):
:param str session_id: The SSH session ID
:param str username: The name of the user who attempts to login
:return: None if the MIC check was successful
- :raises gssapi.GSSException: if the MIC check failed
+ :raises: ``gssapi.GSSException`` -- if the MIC check failed
"""
self._session_id = session_id
self._username = username
@@ -371,7 +361,6 @@ class _SSH_GSSAPI(_SSH_GSSAuth):
Checks if credentials are delegated (server mode).
:return: ``True`` if credentials are delegated, otherwise ``False``
- :rtype: bool
"""
if self._gss_srv_ctxt.delegated_cred is not None:
return True
@@ -384,8 +373,9 @@ class _SSH_GSSAPI(_SSH_GSSAuth):
(server mode).
:param str client_token: The GSS-API token received form the client
- :raise NotImplementedError: Credential delegation is currently not
- supported in server mode
+ :raises:
+ ``NotImplementedError`` -- Credential delegation is currently not
+ supported in server mode
"""
raise NotImplementedError
@@ -427,11 +417,11 @@ class _SSH_SSPI(_SSH_GSSAuth):
("pseudo negotiated" mechanism, because we
support just the krb5 mechanism :-))
:param recv_token: The SSPI token received from the Server
- :raise SSHException: Is raised if the desired mechanism of the client
- is not supported
+ :raises:
+ `.SSHException` -- Is raised if the desired mechanism of the client
+ is not supported
:return: A ``String`` if the SSPI has returned a token or ``None`` if
no token was returned
- :rtype: String or None
"""
self._username = username
self._gss_host = target
@@ -476,8 +466,6 @@ class _SSH_SSPI(_SSH_GSSAuth):
gssapi-keyex:
Returns the MIC token from SSPI with the SSH session ID as
message.
- :rtype: String
- :see: `._ssh_build_mic`
"""
self._session_id = session_id
if not gss_kex:
@@ -501,7 +489,6 @@ class _SSH_SSPI(_SSH_GSSAuth):
if it's not the initial call.
:return: A ``String`` if the SSPI has returned a token or ``None`` if
no token was returned
- :rtype: String or None
"""
self._gss_host = hostname
self._username = username
@@ -522,7 +509,7 @@ class _SSH_SSPI(_SSH_GSSAuth):
:param str session_id: The SSH session ID
:param str username: The name of the user who attempts to login
:return: None if the MIC check was successful
- :raises sspi.error: if the MIC check failed
+ :raises: ``sspi.error`` -- if the MIC check failed
"""
self._session_id = session_id
self._username = username
@@ -548,7 +535,6 @@ class _SSH_SSPI(_SSH_GSSAuth):
Checks if credentials are delegated (server mode).
:return: ``True`` if credentials are delegated, otherwise ``False``
- :rtype: Boolean
"""
return (
self._gss_flags & sspicon.ISC_REQ_DELEGATE and
@@ -562,7 +548,8 @@ class _SSH_SSPI(_SSH_GSSAuth):
(server mode).
:param str client_token: The SSPI token received form the client
- :raise NotImplementedError: Credential delegation is currently not
- supported in server mode
+ :raises:
+ ``NotImplementedError`` -- Credential delegation is currently not
+ supported in server mode
"""
raise NotImplementedError
diff --git a/paramiko/transport.py b/paramiko/transport.py
index 7ff40933..bab23fa1 100644
--- a/paramiko/transport.py
+++ b/paramiko/transport.py
@@ -58,6 +58,7 @@ from paramiko.ed25519key import Ed25519Key
from paramiko.kex_gex import KexGex, KexGexSHA256
from paramiko.kex_group1 import KexGroup1
from paramiko.kex_group14 import KexGroup14
+from paramiko.kex_ecdh_nist import KexNistp256, KexNistp384, KexNistp521
from paramiko.kex_gss import KexGSSGex, KexGSSGroup1, KexGSSGroup14
from paramiko.message import Message
from paramiko.packet import Packetizer, NeedRekeyException
@@ -73,7 +74,6 @@ from paramiko.ssh_exception import (
from paramiko.util import retry_on_signal, ClosingContextManager, clamp_value
-
# for thread cleanup
_active_threads = []
@@ -109,31 +109,35 @@ class Transport(threading.Thread, ClosingContextManager):
'aes192-ctr',
'aes256-ctr',
'aes128-cbc',
- 'blowfish-cbc',
'aes192-cbc',
'aes256-cbc',
+ 'blowfish-cbc',
'3des-cbc',
- 'arcfour128',
- 'arcfour256',
)
_preferred_macs = (
'hmac-sha2-256',
'hmac-sha2-512',
+ 'hmac-sha1',
'hmac-md5',
'hmac-sha1-96',
'hmac-md5-96',
- 'hmac-sha1',
)
_preferred_keys = (
'ssh-ed25519',
+ 'ecdsa-sha2-nistp256',
+ 'ecdsa-sha2-nistp384',
+ 'ecdsa-sha2-nistp521',
'ssh-rsa',
'ssh-dss',
- ) + tuple(ECDSAKey.supported_key_format_identifiers())
+ )
_preferred_kex = (
- 'diffie-hellman-group1-sha1',
- 'diffie-hellman-group14-sha1',
- 'diffie-hellman-group-exchange-sha1',
+ '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',)
@@ -186,18 +190,6 @@ class Transport(threading.Thread, ClosingContextManager):
'block-size': 8,
'key-size': 24
},
- 'arcfour128': {
- 'class': algorithms.ARC4,
- 'mode': None,
- 'block-size': 8,
- 'key-size': 16
- },
- 'arcfour256': {
- 'class': algorithms.ARC4,
- 'mode': None,
- 'block-size': 8,
- 'key-size': 32
- },
}
@@ -214,6 +206,8 @@ class Transport(threading.Thread, ClosingContextManager):
'ssh-rsa': RSAKey,
'ssh-dss': DSSKey,
'ecdsa-sha2-nistp256': ECDSAKey,
+ 'ecdsa-sha2-nistp384': ECDSAKey,
+ 'ecdsa-sha2-nistp521': ECDSAKey,
'ssh-ed25519': Ed25519Key,
}
@@ -224,7 +218,10 @@ class Transport(threading.Thread, ClosingContextManager):
'diffie-hellman-group-exchange-sha256': KexGexSHA256,
'gss-group1-sha1-toWM5Slw5Ew8Mqkay+al2g==': KexGSSGroup1,
'gss-group14-sha1-toWM5Slw5Ew8Mqkay+al2g==': KexGSSGroup14,
- 'gss-gex-sha1-toWM5Slw5Ew8Mqkay+al2g==': KexGSSGex
+ 'gss-gex-sha1-toWM5Slw5Ew8Mqkay+al2g==': KexGSSGex,
+ 'ecdh-sha2-nistp256': KexNistp256,
+ 'ecdh-sha2-nistp384': KexNistp384,
+ 'ecdh-sha2-nistp521': KexNistp521,
}
_compression_info = {
@@ -290,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)
@@ -324,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)
@@ -400,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
@@ -459,7 +452,6 @@ class Transport(threading.Thread, ClosingContextManager):
:param str gss_host: The targets name in the kerberos database
Default: The name of the host to connect to
- :rtype: Void
"""
# We need the FQDN to get this working with SSPI
self.gss_host = socket.getfqdn(gss_host)
@@ -495,8 +487,9 @@ class Transport(threading.Thread, ClosingContextManager):
:param float timeout:
a timeout, in seconds, for SSH2 session negotiation (optional)
- :raises SSHException: if negotiation fails (and no ``event`` was passed
- in)
+ :raises:
+ `.SSHException` -- if negotiation fails (and no ``event`` was
+ passed in)
"""
self.active = True
if event is not None:
@@ -560,8 +553,9 @@ class Transport(threading.Thread, ClosingContextManager):
an object used to perform authentication and create `channels
<.Channel>`
- :raises SSHException: if negotiation fails (and no ``event`` was passed
- in)
+ :raises:
+ `.SSHException` -- if negotiation fails (and no ``event`` was
+ passed in)
"""
if server is None:
server = ServerInterface()
@@ -663,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.
@@ -676,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):
"""
@@ -687,7 +677,7 @@ class Transport(threading.Thread, ClosingContextManager):
string)``. You can get the same effect by calling `.PKey.get_name`
for the key type, and ``str(key)`` for the key string.
- :raises SSHException: if no session is currently active.
+ :raises: `.SSHException` -- if no session is currently active.
:return: public key (`.PKey`) of the remote server
"""
@@ -727,7 +717,8 @@ class Transport(threading.Thread, ClosingContextManager):
:return: a new `.Channel`
- :raises SSHException: if the request is rejected or the session ends
+ :raises:
+ `.SSHException` -- if the request is rejected or the session ends
prematurely
.. versionchanged:: 1.13.4/1.14.3/1.15.3
@@ -750,7 +741,8 @@ class Transport(threading.Thread, ClosingContextManager):
x11 port, ie. 6010)
:return: a new `.Channel`
- :raises SSHException: if the request is rejected or the session ends
+ :raises:
+ `.SSHException` -- if the request is rejected or the session ends
prematurely
"""
return self.open_channel('x11', src_addr=src_addr)
@@ -764,7 +756,7 @@ class Transport(threading.Thread, ClosingContextManager):
:return: a new `.Channel`
- :raises SSHException:
+ :raises: `.SSHException` --
if the request is rejected or the session ends prematurely
"""
return self.open_channel('auth-agent@openssh.com')
@@ -816,7 +808,8 @@ class Transport(threading.Thread, ClosingContextManager):
:return: a new `.Channel` on success
- :raises SSHException: if the request is rejected, the session ends
+ :raises:
+ `.SSHException` -- if the request is rejected, the session ends
prematurely or there is a timeout openning a channel
.. versionchanged:: 1.15
@@ -903,7 +896,8 @@ class Transport(threading.Thread, ClosingContextManager):
:return: the port number (`int`) allocated by the server
- :raises SSHException: if the server refused the TCP forward request
+ :raises:
+ `.SSHException` -- if the server refused the TCP forward request
"""
if not self.active:
raise SSHException('SSH session not active')
@@ -977,8 +971,9 @@ class Transport(threading.Thread, ClosingContextManager):
traffic both ways as the two sides swap keys and do computations. This
method returns when the session has switched to new keys.
- :raises SSHException: if the key renegotiation failed (which causes the
- session to end)
+ :raises:
+ `.SSHException` -- if the key renegotiation failed (which causes
+ the session to end)
"""
self.completion_event = threading.Event()
self._send_kex_init()
@@ -1119,7 +1114,7 @@ class Transport(threading.Thread, ClosingContextManager):
:param bool gss_deleg_creds:
Whether to delegate GSS-API client credentials.
- :raises SSHException: if the SSH2 negotiation fails, the host key
+ :raises: `.SSHException` -- if the SSH2 negotiation fails, the host key
supplied by the server is incorrect, or authentication fails.
"""
if hostkey is not None:
@@ -1193,7 +1188,7 @@ class Transport(threading.Thread, ClosingContextManager):
passed to the `.SubsystemHandler` constructor later.
:param str name: name of the subsystem.
- :param class handler:
+ :param handler:
subclass of `.SubsystemHandler` that handles this subsystem.
"""
try:
@@ -1254,9 +1249,11 @@ class Transport(threading.Thread, ClosingContextManager):
`list` of auth types permissible for the next stage of
authentication (normally empty)
- :raises BadAuthenticationType: if "none" authentication isn't allowed
+ :raises:
+ `.BadAuthenticationType` -- if "none" authentication isn't allowed
by the server for this user
- :raises SSHException: if the authentication failed due to a network
+ :raises:
+ `.SSHException` -- if the authentication failed due to a network
error
.. versionadded:: 1.5
@@ -1307,11 +1304,13 @@ class Transport(threading.Thread, ClosingContextManager):
`list` of auth types permissible for the next stage of
authentication (normally empty)
- :raises BadAuthenticationType: if password authentication isn't
+ :raises:
+ `.BadAuthenticationType` -- if password authentication isn't
allowed by the server for this user (and no event was passed in)
- :raises AuthenticationException: if the authentication failed (and no
+ :raises:
+ `.AuthenticationException` -- if the authentication failed (and no
event was passed in)
- :raises SSHException: if there was a network error
+ :raises: `.SSHException` -- if there was a network error
"""
if (not self.active) or (not self.initial_kex_done):
# we should never try to send the password unless we're on a secure
@@ -1376,11 +1375,13 @@ class Transport(threading.Thread, ClosingContextManager):
`list` of auth types permissible for the next stage of
authentication (normally empty)
- :raises BadAuthenticationType: if public-key authentication isn't
+ :raises:
+ `.BadAuthenticationType` -- if public-key authentication isn't
allowed by the server for this user (and no event was passed in)
- :raises AuthenticationException: if the authentication failed (and no
+ :raises:
+ `.AuthenticationException` -- if the authentication failed (and no
event was passed in)
- :raises SSHException: if there was a network error
+ :raises: `.SSHException` -- if there was a network error
"""
if (not self.active) or (not self.initial_kex_done):
# we should never try to authenticate unless we're on a secure link
@@ -1432,10 +1433,10 @@ class Transport(threading.Thread, ClosingContextManager):
`list` of auth types permissible for the next stage of
authentication (normally empty).
- :raises BadAuthenticationType: if public-key authentication isn't
+ :raises: `.BadAuthenticationType` -- if public-key authentication isn't
allowed by the server for this user
- :raises AuthenticationException: if the authentication failed
- :raises SSHException: if there was a network error
+ :raises: `.AuthenticationException` -- if the authentication failed
+ :raises: `.SSHException` -- if there was a network error
.. versionadded:: 1.5
"""
@@ -1480,11 +1481,12 @@ class Transport(threading.Thread, ClosingContextManager):
:return: list of auth types permissible for the next stage of
authentication (normally empty)
:rtype: list
- :raise BadAuthenticationType: if gssapi-with-mic isn't
+ :raises: `.BadAuthenticationType` -- if gssapi-with-mic isn't
allowed by the server (and no event was passed in)
- :raise AuthenticationException: if the authentication failed (and no
+ :raises:
+ `.AuthenticationException` -- if the authentication failed (and no
event was passed in)
- :raise SSHException: if there was a network error
+ :raises: `.SSHException` -- if there was a network error
"""
if (not self.active) or (not self.initial_kex_done):
# we should never try to authenticate unless we're on a secure link
@@ -1504,12 +1506,12 @@ class Transport(threading.Thread, ClosingContextManager):
:returns:
a `list` of auth types permissible for the next stage of
authentication (normally empty)
- :raises BadAuthenticationType:
+ :raises: `.BadAuthenticationType` --
if GSS-API Key Exchange was not performed (and no event was passed
in)
- :raises AuthenticationException:
+ :raises: `.AuthenticationException` --
if the authentication failed (and no event was passed in)
- :raises SSHException: if there was a network error
+ :raises: `.SSHException` -- if there was a network error
"""
if (not self.active) or (not self.initial_kex_done):
# we should never try to authenticate unless we're on a secure link
@@ -1731,21 +1733,6 @@ class Transport(threading.Thread, ClosingContextManager):
def _get_cipher(self, name, key, iv, operation):
if name not in self._cipher_info:
raise SSHException('Unknown client cipher ' + name)
- if name in ('arcfour128', 'arcfour256'):
- # arcfour cipher
- cipher = Cipher(
- self._cipher_info[name]['class'](key),
- None,
- backend=default_backend()
- )
- if operation is self._ENCRYPT:
- engine = cipher.encryptor()
- else:
- engine = cipher.decryptor()
- # as per RFC 4345, the first 1536 bytes of keystream
- # generated by the cipher MUST be discarded
- engine.encrypt(" " * 1536)
- return engine
else:
cipher = Cipher(
self._cipher_info[name]['class'](key),
@@ -2113,6 +2100,9 @@ class Transport(threading.Thread, ClosingContextManager):
self.host_key_type = agreed_keys[0]
if self.server_mode and (self.get_server_key() is None):
raise SSHException('Incompatible ssh peer (can\'t match requested host key type)') # noqa
+ self._log_agreement(
+ 'HostKey', agreed_keys[0], agreed_keys[0]
+ )
if self.server_mode:
agreed_local_ciphers = list(filter(
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/setup.py b/setup.py
index 4cf477ff..1234bfa5 100644
--- a/setup.py
+++ b/setup.py
@@ -72,9 +72,10 @@ setup(
'Programming Language :: Python :: 3.3',
'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.5',
+ 'Programming Language :: Python :: 3.6',
],
install_requires=[
- 'bcrypt>=3.0.0',
+ 'bcrypt>=3.1.3',
'cryptography>=1.5',
'pynacl>=1.0.1',
'pyasn1>=0.1.7',
diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst
index e81dc69a..dea044bf 100644
--- a/sites/www/changelog.rst
+++ b/sites/www/changelog.rst
@@ -2,14 +2,113 @@
Changelog
=========
+* :release:`2.2.1 <2017-06-13>`
+* :bug:`993` Ed25519 host keys were not comparable/hashable, causing an
+ exception if such a key existed in a ``known_hosts`` file. Thanks to Oleh
+ Prypin for the report and Pierce Lopez for the fix.
+* :bug:`990` The (added in 2.2.0) ``bcrypt`` dependency should have been on
+ version 3.1.3 or greater (was initially set to 3.0.0 or greater.) Thanks to
+ Paul Howarth for the report.
+* :release:`2.2.0 <2017-06-09>`
+* :release:`2.1.3 <2017-06-09>`
+* :release:`2.0.6 <2017-06-09>`
+* :release:`1.18.3 <2017-06-09>`
+* :release:`1.17.5 <2017-06-09>`
+* :bug:`865` SSHClient now requests the type of host key it has (e.g. from
+ known_hosts) and does not consider a different type to be a "Missing" host
+ key. This fixes a common case where an ECDSA key is in known_hosts and the
+ server also has an RSA host key. Thanks to Pierce Lopez.
+* :support:`906 (1.18+)` Clean up a handful of outdated imports and related
+ tweaks. Thanks to Pierce Lopez.
+* :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
+ for catch & patch.
+* :feature:`857` Allow `SSHClient.set_missing_host_key_policy
+ <paramiko.client.SSHClient.set_missing_host_key_policy>` to accept policy
+ classes _or_ instances, instead of only instances, thus fixing a
+ 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``. 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.
+* :bug:`900` (via :issue:`911`) Prefer newer ``ecdsa-sha2-nistp`` keys over RSA
+ and DSA keys during host key selection. This improves compatibility with
+ OpenSSH, both in terms of general behavior, and also re: ability to properly
+ leverage OpenSSH-modified ``known_hosts`` files. Credit: ``@kasdoe`` for
+ original report/PR and Pierce Lopez for the second draft.
+* :bug:`794` (via :issue:`981`) Prior support for ``ecdsa-sha2-nistp(384|521)``
+ algorithms didn't fully extend to covering host keys, preventing connection
+ to hosts which only offer these key types and no others. This is now fixed.
+ Thanks to ``@ncoult`` and ``@kasdoe`` for reports and Pierce Lopez for the
+ patch.
* :feature:`325` (via :issue:`972`) Add Ed25519 support, for both host keys
and user authentication. Big thanks to Alex Gaynor for the patch.
+
+ .. note::
+ This change adds the ``bcrypt`` and ``pynacl`` Python libraries as
+ dependencies. No C-level dependencies beyond those previously required (for
+ Cryptography) have been added.
+
* :support:`974 backported` Overhaul the codebase to be PEP-8, etc, compliant
(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>`
@@ -91,7 +190,7 @@ Changelog
* :bug:`334 (1.17+)` Make the ``subprocess`` import in ``proxy.py`` lazy so
users on platforms without it (such as Google App Engine) can import Paramiko
successfully. (Relatedly, make it easier to tweak an active socket check
- timeout [in `Transport <paramko.transport.Transport>`] which was previously
+ timeout [in `Transport <paramiko.transport.Transport>`] which was previously
hardcoded.) Credit: Shinya Okano.
* :support:`854 backported (1.17+)` Fix incorrect docstring/param-list for
`Transport.auth_gssapi_keyex
@@ -156,10 +255,10 @@ Changelog
``proxycommand`` key in parsed config structures). Thanks to Pat Brisbin for
the catch.
* :bug:`676` (via :issue:`677`) Fix a backwards incompatibility issue that
- cropped up in `SFTPFile.prefetch <~paramiko.sftp_file.prefetch>` re: the
- erroneously non-optional ``file_size`` parameter. Should only affect users
- who manually call ``prefetch``. Thanks to ``@stevevanhooser`` for catch &
- patch.
+ cropped up in `SFTPFile.prefetch <paramiko.sftp_file.SFTPFile.prefetch>` re:
+ the erroneously non-optional ``file_size`` parameter. Should only affect
+ users who manually call ``prefetch``. Thanks to ``@stevevanhooser`` for catch
+ & patch.
* :feature:`394` Replace PyCrypto with the Python Cryptographic Authority
(PyCA) 'Cryptography' library suite. This improves security, installability,
and performance; adds PyPy support; and much more.
@@ -249,7 +348,7 @@ Changelog
* :release:`1.15.4 <2015-11-02>`
* :release:`1.14.3 <2015-11-02>`
* :release:`1.13.4 <2015-11-02>`
-* :bug:`366` Fix `~paramiko.sftp_attributes.SFTPAttributes` so its string
+* :bug:`366` Fix `~paramiko.sftp_attr.SFTPAttributes` so its string
representation doesn't raise exceptions on empty/initialized instances. Patch
by Ulrich Petri.
* :bug:`359` Use correct attribute name when trying to use Python 3's
@@ -360,8 +459,9 @@ Changelog
* :release:`1.15.1 <2014-09-22>`
* :bug:`399` SSH agent forwarding (potentially other functionality as
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.
+ arguments for `~paramiko.transport.Transport` (thanks to a botched merge).
+ This has been corrected. Thanks to Dylan Thacker-Smith for the report &
+ patch.
* :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.
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 a340be00..e912d5b2 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):
@@ -119,7 +122,11 @@ class SSHClientTest (unittest.TestCase):
allowed_keys = FINGERPRINTS.keys()
self.socks, addr = self.sockl.accept()
self.ts = paramiko.Transport(self.socks)
- host_key = paramiko.RSAKey.from_private_key_file(test_path('test_rsa.key'))
+ keypath = test_path('test_rsa.key')
+ host_key = paramiko.RSAKey.from_private_key_file(keypath)
+ self.ts.add_server_key(host_key)
+ keypath = test_path('test_ecdsa_256.key')
+ host_key = paramiko.ECDSAKey.from_private_key_file(keypath)
self.ts.add_server_key(host_key)
server = NullServer(allowed_keys=allowed_keys)
if delay:
@@ -246,8 +253,9 @@ class SSHClientTest (unittest.TestCase):
verify that SSHClient's AutoAddPolicy works.
"""
threading.Thread(target=self._run).start()
- host_key = paramiko.RSAKey.from_private_key_file(test_path('test_rsa.key'))
- public_host_key = paramiko.RSAKey(data=host_key.asbytes())
+ hostname = '[%s]:%d' % (self.addr, self.port)
+ key_file = test_path('test_ecdsa_256.key')
+ public_host_key = paramiko.ECDSAKey.from_private_key_file(key_file)
self.tc = paramiko.SSHClient()
self.tc.set_missing_host_key_policy(paramiko.AutoAddPolicy())
@@ -260,7 +268,8 @@ class SSHClientTest (unittest.TestCase):
self.assertEqual('slowdive', self.ts.get_username())
self.assertEqual(True, self.ts.is_authenticated())
self.assertEqual(1, len(self.tc.get_host_keys()))
- self.assertEqual(public_host_key, self.tc.get_host_keys()['[%s]:%d' % (self.addr, self.port)]['ssh-rsa'])
+ new_host_key = list(self.tc.get_host_keys()[hostname].values())[0]
+ self.assertEqual(public_host_key, new_host_key)
def test_5_save_host_keys(self):
"""
@@ -294,13 +303,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 +324,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 +390,64 @@ 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 _client_host_key_bad(self, host_key):
+ threading.Thread(target=self._run).start()
+ hostname = '[%s]:%d' % (self.addr, self.port)
+
+ self.tc = paramiko.SSHClient()
+ self.tc.set_missing_host_key_policy(paramiko.WarningPolicy())
+ known_hosts = self.tc.get_host_keys()
+ known_hosts.add(hostname, host_key.get_name(), host_key)
+
+ self.assertRaises(
+ paramiko.BadHostKeyException,
+ self.tc.connect,
+ password='pygmalion',
+ **self.connect_kwargs
+ )
+
+ def _client_host_key_good(self, ktype, kfile):
+ threading.Thread(target=self._run).start()
+ hostname = '[%s]:%d' % (self.addr, self.port)
+
+ self.tc = paramiko.SSHClient()
+ self.tc.set_missing_host_key_policy(paramiko.RejectPolicy())
+ host_key = ktype.from_private_key_file(test_path(kfile))
+ known_hosts = self.tc.get_host_keys()
+ known_hosts.add(hostname, host_key.get_name(), host_key)
+
+ self.tc.connect(password='pygmalion', **self.connect_kwargs)
+ self.event.wait(1.0)
+ self.assertTrue(self.event.is_set())
+ self.assertTrue(self.ts.is_active())
+ self.assertEqual(True, self.ts.is_authenticated())
+
+ def test_host_key_negotiation_1(self):
+ host_key = paramiko.ECDSAKey.generate()
+ self._client_host_key_bad(host_key)
+
+ def test_host_key_negotiation_2(self):
+ host_key = paramiko.RSAKey.generate(2048)
+ self._client_host_key_bad(host_key)
+
+ def test_host_key_negotiation_3(self):
+ self._client_host_key_good(paramiko.ECDSAKey, 'test_ecdsa_256.key')
+
+ def test_host_key_negotiation_4(self):
+ self._client_host_key_good(paramiko.RSAKey, 'test_rsa.key')
+
def test_update_environment(self):
"""
Verify that environment variables can be set by the client.
@@ -418,3 +482,20 @@ class SSHClientTest (unittest.TestCase):
'Expected original SSHException in exception')
else:
self.assertFalse(False, 'SSHException was not thrown.')
+
+
+ def test_missing_key_policy_accepts_classes_or_instances(self):
+ """
+ Client.missing_host_key_policy() can take classes or instances.
+ """
+ # AN ACTUAL UNIT TEST?! GOOD LORD
+ # (But then we have to test a private API...meh.)
+ client = paramiko.SSHClient()
+ # Default
+ assert isinstance(client._policy, paramiko.RejectPolicy)
+ # Hand in an instance (classic behavior)
+ client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
+ assert isinstance(client._policy, paramiko.AutoAddPolicy)
+ # Hand in just the class (new behavior)
+ client.set_missing_host_key_policy(paramiko.AutoAddPolicy)
+ assert isinstance(client._policy, paramiko.AutoAddPolicy)
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_kex.py b/tests/test_kex.py
index 19804fbf..b7f588f7 100644
--- a/tests/test_kex.py
+++ b/tests/test_kex.py
@@ -20,7 +20,7 @@
Some unit tests for the key exchange protocols.
"""
-from binascii import hexlify
+from binascii import hexlify, unhexlify
import os
import unittest
@@ -29,11 +29,24 @@ from paramiko.kex_group1 import KexGroup1
from paramiko.kex_gex import KexGex, KexGexSHA256
from paramiko import Message
from paramiko.common import byte_chr
+from paramiko.kex_ecdh_nist import KexNistp256
+from cryptography.hazmat.backends import default_backend
+from cryptography.hazmat.primitives.asymmetric import ec
def dummy_urandom(n):
return byte_chr(0xcc) * n
+def dummy_generate_key_pair(obj):
+ private_key_value = 94761803665136558137557783047955027733968423115106677159790289642479432803037
+ public_key_numbers = "042bdab212fa8ba1b7c843301682a4db424d307246c7e1e6083c41d9ca7b098bf30b3d63e2ec6278488c135360456cc054b3444ecc45998c08894cbc1370f5f989"
+ public_key_numbers_obj = ec.EllipticCurvePublicNumbers.from_encoded_point(ec.SECP256R1(), unhexlify(public_key_numbers))
+ obj.P = ec.EllipticCurvePrivateNumbers(private_value=private_key_value, public_numbers=public_key_numbers_obj).private_key(default_backend())
+ if obj.transport.server_mode:
+ obj.Q_S = ec.EllipticCurvePublicNumbers.from_encoded_point(ec.SECP256R1(), unhexlify(public_key_numbers)).public_key(default_backend())
+ return
+ obj.Q_C = ec.EllipticCurvePublicNumbers.from_encoded_point(ec.SECP256R1(), unhexlify(public_key_numbers)).public_key(default_backend())
+
class FakeKey (object):
def __str__(self):
@@ -93,9 +106,12 @@ class KexTest (unittest.TestCase):
def setUp(self):
self._original_urandom = os.urandom
os.urandom = dummy_urandom
+ self._original_generate_key_pair = KexNistp256._generate_key_pair
+ KexNistp256._generate_key_pair = dummy_generate_key_pair
def tearDown(self):
os.urandom = self._original_urandom
+ KexNistp256._generate_key_pair = self._original_generate_key_pair
def test_1_group1_client(self):
transport = FakeTransport()
@@ -369,4 +385,43 @@ class KexTest (unittest.TestCase):
self.assertEqual(x, hexlify(transport._message.asbytes()).upper())
self.assertTrue(transport._activated)
+ def test_11_kex_nistp256_client(self):
+ K = 91610929826364598472338906427792435253694642563583721654249504912114314269754
+ transport = FakeTransport()
+ transport.server_mode = False
+ kex = KexNistp256(transport)
+ kex.start_kex()
+ self.assertEqual((paramiko.kex_ecdh_nist._MSG_KEXECDH_REPLY,), transport._expect)
+
+ #fake reply
+ msg = Message()
+ msg.add_string('fake-host-key')
+ Q_S = unhexlify("043ae159594ba062efa121480e9ef136203fa9ec6b6e1f8723a321c16e62b945f573f3b822258cbcd094b9fa1c125cbfe5f043280893e66863cc0cb4dccbe70210")
+ msg.add_string(Q_S)
+ msg.add_string('fake-sig')
+ msg.rewind()
+ kex.parse_next(paramiko.kex_ecdh_nist._MSG_KEXECDH_REPLY, msg)
+ H = b'BAF7CE243A836037EB5D2221420F35C02B9AB6C957FE3BDE3369307B9612570A'
+ self.assertEqual(K, kex.transport._K)
+ self.assertEqual(H, hexlify(transport._H).upper())
+ self.assertEqual((b'fake-host-key', b'fake-sig'), transport._verify)
+ self.assertTrue(transport._activated)
+
+ def test_12_kex_nistp256_server(self):
+ K = 91610929826364598472338906427792435253694642563583721654249504912114314269754
+ transport = FakeTransport()
+ transport.server_mode = True
+ kex = KexNistp256(transport)
+ kex.start_kex()
+ self.assertEqual((paramiko.kex_ecdh_nist._MSG_KEXECDH_INIT,), transport._expect)
+ #fake init
+ msg=Message()
+ Q_C = unhexlify("043ae159594ba062efa121480e9ef136203fa9ec6b6e1f8723a321c16e62b945f573f3b822258cbcd094b9fa1c125cbfe5f043280893e66863cc0cb4dccbe70210")
+ H = b'2EF4957AFD530DD3F05DBEABF68D724FACC060974DA9704F2AEE4C3DE861E7CA'
+ msg.add_string(Q_C)
+ msg.rewind()
+ kex.parse_next(paramiko.kex_ecdh_nist._MSG_KEXECDH_INIT, msg)
+ self.assertEqual(K, transport._K)
+ self.assertTrue(transport._activated)
+ self.assertEqual(H, hexlify(transport._H).upper())
diff --git a/tests/test_pkey.py b/tests/test_pkey.py
index a26ff170..9bb3c44c 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)
@@ -435,5 +455,28 @@ class KeyTest(unittest.TestCase):
key2 = Ed25519Key.from_private_key_file(
test_path('test_ed25519_password.key'), b'abc123'
)
-
self.assertNotEqual(key1.asbytes(), key2.asbytes())
+
+ def test_ed25519_compare(self):
+ # verify that the private & public keys compare equal
+ key = Ed25519Key.from_private_key_file(test_path('test_ed25519.key'))
+ self.assertEqual(key, key)
+ pub = Ed25519Key(data=key.asbytes())
+ self.assertTrue(key.can_sign())
+ self.assertTrue(not pub.can_sign())
+ self.assertEqual(key, pub)
+
+ 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 2ebdf854..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
@@ -165,6 +166,15 @@ class TransportTest(unittest.TestCase):
except TypeError:
pass
+ def test_1b_security_options_reset(self):
+ o = self.tc.get_security_options()
+ # should not throw any exceptions
+ o.ciphers = o.ciphers
+ o.digests = o.digests
+ o.key_types = o.key_types
+ o.kex = o.kex
+ o.compression = o.compression
+
def test_2_compute_key(self):
self.tc.K = 123281095979686581523377256114209720774539068973101330872763622971399429481072519713536292772709507296759612401802191955568143056534122385270077606457721553469730659233569339356140085284052436697480759510519672848743794433460113118986816826624865291116513647975790797391795651716378444844877749505443714557929
self.tc.H = b'\x0C\x83\x07\xCD\xE6\x85\x6F\xF3\x0B\xA9\x36\x84\xEB\x0F\x04\xC2\x52\x0E\x9E\xD3'
@@ -849,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)