summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJeff Forcier <jeff@bitprophet.org>2019-05-31 20:20:24 -0400
committerJeff Forcier <jeff@bitprophet.org>2019-05-31 20:20:24 -0400
commite32ac1fc926cd9e2e4998c12aecafc37862f13ef (patch)
tree51b1508b7364a119e1ec6ff069c198eaca32e17c
parentf3af0b3e697adc8902039b21fde93871048160e4 (diff)
parent01389cfc2de782a1884ffcf8e35ff659cb4d38c3 (diff)
downloadparamiko-e32ac1fc926cd9e2e4998c12aecafc37862f13ef.tar.gz
Merge branch 'master' into 1311-int
-rw-r--r--.travis.yml21
-rw-r--r--README.rst2
-rw-r--r--dev-requirements.txt6
-rw-r--r--paramiko/__init__.py2
-rw-r--r--paramiko/agent.py4
-rw-r--r--paramiko/auth_handler.py4
-rw-r--r--paramiko/client.py6
-rw-r--r--paramiko/common.py31
-rw-r--r--paramiko/config.py83
-rw-r--r--paramiko/ecdsakey.py4
-rw-r--r--paramiko/hostkeys.py6
-rw-r--r--paramiko/kex_ecdh_nist.py37
-rw-r--r--paramiko/pkey.py6
-rw-r--r--paramiko/py3compat.py41
-rw-r--r--paramiko/sftp_client.py4
-rw-r--r--paramiko/ssh_gss.py13
-rw-r--r--paramiko/util.py18
-rw-r--r--setup.cfg4
-rw-r--r--setup.py9
-rw-r--r--sites/shared_conf.py1
-rw-r--r--sites/www/changelog.rst47
-rw-r--r--sites/www/contact.rst2
-rw-r--r--sites/www/installing-1.x.rst2
-rw-r--r--sites/www/installing.rst10
-rw-r--r--tests/test_auth.py18
-rw-r--r--tests/test_config.py74
-rw-r--r--tests/test_kex.py12
27 files changed, 352 insertions, 115 deletions
diff --git a/.travis.yml b/.travis.yml
index 11de689c..2a496e55 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,3 +1,4 @@
+dist: xenial
language: python
sudo: false
cache:
@@ -8,21 +9,23 @@ python:
- "3.4"
- "3.5"
- "3.6"
- - "3.7-dev"
- - "pypy-5.6.0"
+ - "3.7"
+ - "3.8-dev"
+ - "pypy"
+ - "pypy3"
matrix:
allow_failures:
- - python: "3.7-dev"
+ - python: "3.8-dev"
# Explicitly test against our oldest supported cryptography.io, in addition
# to whatever the latest default is.
include:
- python: 2.7
- env: "CRYPTO_BEFORE=1.6"
- - python: 3.6
- env: "CRYPTO_BEFORE=1.6"
+ env: "OLDEST_CRYPTO=2.5"
+ - python: 3.7
+ env: "OLDEST_CRYPTO=2.5"
- python: 2.7
env: "USE_K5TEST=yes"
- - python: 3.6
+ - python: 3.7
env: "USE_K5TEST=yes"
install:
# Ensure modern pip/etc to avoid some issues w/ older worker environs
@@ -30,8 +33,8 @@ install:
# Grab a specific version of Cryptography if desired. Doing this before other
# installations ensures we don't have to do any downgrading/overriding.
- |
- if [[ -n "$CRYPTO_BEFORE" ]]; then
- pip install "cryptography<${CRYPTO_BEFORE}"
+ if [[ -n "$OLDEST_CRYPTO" ]]; then
+ pip install "cryptography==${OLDEST_CRYPTO}"
fi
# Self-install for setup.py-driven deps
- pip install -e .
diff --git a/README.rst b/README.rst
index 6e49bd68..c918652f 100644
--- a/README.rst
+++ b/README.rst
@@ -11,7 +11,7 @@ Paramiko
:Paramiko: Python SSH module
:Copyright: Copyright (c) 2003-2009 Robey Pointer <robeypointer@gmail.com>
-:Copyright: Copyright (c) 2013-2017 Jeff Forcier <jeff@bitprophet.org>
+:Copyright: Copyright (c) 2013-2019 Jeff Forcier <jeff@bitprophet.org>
:License: `LGPL <https://www.gnu.org/copyleft/lesser.html>`_
:Homepage: http://www.paramiko.org/
:API docs: http://docs.paramiko.org
diff --git a/dev-requirements.txt b/dev-requirements.txt
index 8814627f..4c118991 100644
--- a/dev-requirements.txt
+++ b/dev-requirements.txt
@@ -5,16 +5,16 @@ invocations>=1.2.0,<2.0
pytest>=3.2,<3.3
pytest-relaxed==1.1.4
# pytest-xdist for test dir watching and the inv guard task
-pytest-xdist>=1.22,<2.0
+pytest-xdist>=1.22,<1.25.0
mock==2.0.0
# Linting!
-flake8==2.4.0
+flake8==3.6.0
# Coverage!
coverage==3.7.1
codecov==1.6.3
# Documentation tools
sphinx>=1.4,<1.7
-alabaster>=0.7,<2.0
+alabaster==0.7.12
releases>=1.5,<2.0
# Release tools
semantic_version>=2.4,<2.5
diff --git a/paramiko/__init__.py b/paramiko/__init__.py
index f77a2bcc..ebfa72a8 100644
--- a/paramiko/__init__.py
+++ b/paramiko/__init__.py
@@ -100,6 +100,8 @@ __all__ = [
"Channel",
"ChannelException",
"DSSKey",
+ "ECDSAKey",
+ "Ed25519Key",
"HostKeys",
"Message",
"MissingHostKeyPolicy",
diff --git a/paramiko/agent.py b/paramiko/agent.py
index 0630ebf3..622b95e4 100644
--- a/paramiko/agent.py
+++ b/paramiko/agent.py
@@ -80,8 +80,8 @@ class AgentSSH(object):
def _send_message(self, msg):
msg = asbytes(msg)
self._conn.send(struct.pack(">I", len(msg)) + msg)
- l = self._read_all(4)
- msg = Message(self._read_all(struct.unpack(">I", l)[0]))
+ data = self._read_all(4)
+ msg = Message(self._read_all(struct.unpack(">I", data)[0]))
return ord(msg.get_byte()), msg
def _read_all(self, wanted):
diff --git a/paramiko/auth_handler.py b/paramiko/auth_handler.py
index 26605a16..5c7d6be6 100644
--- a/paramiko/auth_handler.py
+++ b/paramiko/auth_handler.py
@@ -61,7 +61,7 @@ from paramiko.common import (
cMSG_USERAUTH_BANNER,
)
from paramiko.message import Message
-from paramiko.py3compat import bytestring
+from paramiko.py3compat import b
from paramiko.ssh_exception import (
SSHException,
AuthenticationException,
@@ -280,7 +280,7 @@ class AuthHandler(object):
m.add_string(self.auth_method)
if self.auth_method == "password":
m.add_boolean(False)
- password = bytestring(self.password)
+ password = b(self.password)
m.add_string(password)
elif self.auth_method == "publickey":
m.add_boolean(True)
diff --git a/paramiko/client.py b/paramiko/client.py
index 2538d582..6bf479d4 100644
--- a/paramiko/client.py
+++ b/paramiko/client.py
@@ -476,6 +476,9 @@ class SSHClient(ClosingContextManager):
Python
:param int timeout:
set command's channel timeout. See `.Channel.settimeout`
+ :param bool get_pty:
+ Request a pseudo-terminal from the server (default ``False``).
+ See `.Channel.get_pty`
:param dict environment:
a dict of shell environment variables, to be merged into the
default environment that the remote command executes within.
@@ -489,6 +492,9 @@ class SSHClient(ClosingContextManager):
3-tuple
:raises: `.SSHException` -- if the server fails to execute the command
+
+ .. versionchanged:: 1.10
+ Added the ``get_pty`` kwarg.
"""
chan = self._transport.open_session(timeout=timeout)
if get_pty:
diff --git a/paramiko/common.py b/paramiko/common.py
index 87d3dcf6..7bd0cb10 100644
--- a/paramiko/common.py
+++ b/paramiko/common.py
@@ -20,7 +20,7 @@
Common constants and global variables.
"""
import logging
-from paramiko.py3compat import byte_chr, PY2, bytes_types, text_type, long
+from paramiko.py3compat import byte_chr, PY2, long, b
(
MSG_DISCONNECT,
@@ -191,17 +191,24 @@ else:
def asbytes(s):
- """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
+ """
+ Coerce to bytes if possible or return unchanged.
+ """
+ try:
+ # Attempt to run through our version of b(), which does the Right Thing
+ # for string/unicode/buffer (Py2) or bytes/str (Py3), and raises
+ # TypeError if it's not one of those types.
+ return b(s)
+ except TypeError:
+ try:
+ # If it wasn't a string/byte/buffer type object, try calling an
+ # asbytes() method, which many of our internal classes implement.
+ return s.asbytes()
+ except AttributeError:
+ # Finally, just do nothing & assume this object is sufficiently
+ # byte-y or buffer-y that everything will work out (or that callers
+ # are capable of handling whatever it is.)
+ return s
xffffffff = long(0xffffffff)
diff --git a/paramiko/config.py b/paramiko/config.py
index 21c9dab8..aeb59593 100644
--- a/paramiko/config.py
+++ b/paramiko/config.py
@@ -95,7 +95,7 @@ class SSHConfig(object):
def lookup(self, hostname):
"""
- Return a dict of config options for a given hostname.
+ Return a dict (`SSHConfigDict`) of config options for a given hostname.
The host-matching rules of OpenSSH's ``ssh_config`` man page are used:
For each parameter, the first obtained value will be used. The
@@ -111,7 +111,17 @@ class SSHConfig(object):
``"port"``, not ``"Port"``. The values are processed according to the
rules for substitution variable expansion in ``ssh_config``.
+ Finally, please see the docs for `SSHConfigDict` for deeper info on
+ features such as optional type conversion methods, e.g.::
+
+ conf = my_config.lookup('myhost')
+ assert conf['passwordauthentication'] == 'yes'
+ assert conf.as_bool('passwordauthentication') is True
+
:param str hostname: the hostname to lookup
+
+ .. versionchanged:: 2.5
+ Returns `SSHConfigDict` objects instead of dict literals.
"""
matches = [
config
@@ -119,7 +129,7 @@ class SSHConfig(object):
if self._allowed(config["host"], hostname)
]
- ret = {}
+ ret = SSHConfigDict()
for match in matches:
for key, value in match["config"].items():
if key not in ret:
@@ -291,3 +301,72 @@ class LazyFqdn(object):
# Cache
self.fqdn = fqdn
return self.fqdn
+
+
+class SSHConfigDict(dict):
+ """
+ A dictionary wrapper/subclass for per-host configuration structures.
+
+ This class introduces some usage niceties for consumers of `SSHConfig`,
+ specifically around the issue of variable type conversions: normal value
+ access yields strings, but there are now methods such as `as_bool` and
+ `as_int` that yield casted values instead.
+
+ For example, given the following ``ssh_config`` file snippet::
+
+ Host foo.example.com
+ PasswordAuthentication no
+ Compression yes
+ ServerAliveInterval 60
+
+ the following code highlights how you can access the raw strings as well as
+ usefully Python type-casted versions (recalling that keys are all
+ normalized to lowercase first)::
+
+ my_config = SSHConfig()
+ my_config.parse(open('~/.ssh/config'))
+ conf = my_config.lookup('foo.example.com')
+
+ assert conf['passwordauthentication'] == 'no'
+ assert conf.as_bool('passwordauthentication') is False
+ assert conf['compression'] == 'yes'
+ assert conf.as_bool('compression') is True
+ assert conf['serveraliveinterval'] == '60'
+ assert conf.as_int('serveraliveinterval') == 60
+
+ .. versionadded:: 2.5
+ """
+
+ def __init__(self, *args, **kwargs):
+ # Hey, guess what? Python 2's userdict is an old-style class!
+ super(SSHConfigDict, self).__init__(*args, **kwargs)
+
+ def as_bool(self, key):
+ """
+ Express given key's value as a boolean type.
+
+ Typically, this is used for ``ssh_config``'s pseudo-boolean values
+ which are either ``"yes"`` or ``"no"``. In such cases, ``"yes"`` yields
+ ``True`` and any other value becomes ``False``.
+
+ .. note::
+ If (for whatever reason) the stored value is already boolean in
+ nature, it's simply returned.
+
+ .. versionadded:: 2.5
+ """
+ val = self[key]
+ if isinstance(val, bool):
+ return val
+ return val.lower() == "yes"
+
+ def as_int(self, key):
+ """
+ Express given key's value as an integer, if possible.
+
+ This method will raise ``ValueError`` or similar if the value is not
+ int-appropriate, same as the builtin `int` type.
+
+ .. versionadded:: 2.5
+ """
+ return int(self[key])
diff --git a/paramiko/ecdsakey.py b/paramiko/ecdsakey.py
index b73a969e..353c5f9e 100644
--- a/paramiko/ecdsakey.py
+++ b/paramiko/ecdsakey.py
@@ -160,12 +160,12 @@ class ECDSAKey(PKey):
pointinfo = msg.get_binary()
try:
- numbers = ec.EllipticCurvePublicNumbers.from_encoded_point(
+ key = ec.EllipticCurvePublicKey.from_encoded_point(
self.ecdsa_curve.curve_class(), pointinfo
)
+ self.verifying_key = key
except ValueError:
raise SSHException("Invalid public key")
- self.verifying_key = numbers.public_key(backend=default_backend())
@classmethod
def supported_key_format_identifiers(cls):
diff --git a/paramiko/hostkeys.py b/paramiko/hostkeys.py
index f31b8819..d0660cc8 100644
--- a/paramiko/hostkeys.py
+++ b/paramiko/hostkeys.py
@@ -19,8 +19,12 @@
import binascii
import os
+import sys
-from collections import MutableMapping
+if sys.version_info[:2] >= (3, 3):
+ from collections.abc import MutableMapping
+else:
+ from collections import MutableMapping
from hashlib import sha1
from hmac import HMAC
diff --git a/paramiko/kex_ecdh_nist.py b/paramiko/kex_ecdh_nist.py
index 1d87442a..ad5c9c79 100644
--- a/paramiko/kex_ecdh_nist.py
+++ b/paramiko/kex_ecdh_nist.py
@@ -9,6 +9,7 @@ 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 cryptography.hazmat.primitives import serialization
from binascii import hexlify
_MSG_KEXECDH_INIT, _MSG_KEXECDH_REPLY = range(30, 32)
@@ -36,7 +37,12 @@ class KexNistp256:
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())
+ m.add_string(
+ self.Q_C.public_bytes(
+ serialization.Encoding.X962,
+ serialization.PublicFormat.UncompressedPoint,
+ )
+ )
self.transport._send_message(m)
self.transport._expect_packet(_MSG_KEXECDH_REPLY)
@@ -58,11 +64,11 @@ class KexNistp256:
def _parse_kexecdh_init(self, m):
Q_C_bytes = m.get_string()
- self.Q_C = ec.EllipticCurvePublicNumbers.from_encoded_point(
+ self.Q_C = ec.EllipticCurvePublicKey.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 = self.P.exchange(ec.ECDH(), self.Q_C)
K = long(hexlify(K), 16)
# compute exchange hash
hm = Message()
@@ -75,7 +81,12 @@ class KexNistp256:
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_string(
+ self.Q_S.public_bytes(
+ serialization.Encoding.X962,
+ serialization.PublicFormat.UncompressedPoint,
+ )
+ )
hm.add_mpint(long(K))
H = self.hash_algo(hm.asbytes()).digest()
self.transport._set_K_H(K, H)
@@ -84,7 +95,12 @@ class KexNistp256:
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(
+ self.Q_S.public_bytes(
+ serialization.Encoding.X962,
+ serialization.PublicFormat.UncompressedPoint,
+ )
+ )
m.add_string(sig)
self.transport._send_message(m)
self.transport._activate_outbound()
@@ -92,11 +108,11 @@ class KexNistp256:
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.Q_S = ec.EllipticCurvePublicKey.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 = self.P.exchange(ec.ECDH(), self.Q_S)
K = long(hexlify(K), 16)
# compute exchange hash and verify signature
hm = Message()
@@ -108,7 +124,12 @@ class KexNistp256:
)
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(
+ self.Q_C.public_bytes(
+ serialization.Encoding.X962,
+ serialization.PublicFormat.UncompressedPoint,
+ )
+ )
hm.add_string(Q_S_bytes)
hm.add_mpint(K)
self.transport._set_K_H(K, self.hash_algo(hm.asbytes()).digest())
diff --git a/paramiko/pkey.py b/paramiko/pkey.py
index fa014800..e37f7751 100644
--- a/paramiko/pkey.py
+++ b/paramiko/pkey.py
@@ -291,10 +291,10 @@ class PKey(object):
headers = {}
start += 1
while start < len(lines):
- l = lines[start].split(": ")
- if len(l) == 1:
+ line = lines[start].split(": ")
+ if len(line) == 1:
break
- headers[l[0].lower()] = l[1].strip()
+ headers[line[0].lower()] = line[1].strip()
start += 1
# find end
end = start
diff --git a/paramiko/py3compat.py b/paramiko/py3compat.py
index cbe20ca6..0f80e19f 100644
--- a/paramiko/py3compat.py
+++ b/paramiko/py3compat.py
@@ -2,29 +2,28 @@ import sys
import base64
__all__ = [
+ "BytesIO",
+ "MAXSIZE",
"PY2",
- "string_types",
- "integer_types",
- "text_type",
- "bytes_types",
+ "StringIO",
+ "b",
+ "b2s",
+ "builtins",
+ "byte_chr",
+ "byte_mask",
+ "byte_ord",
"bytes",
- "long",
- "input",
+ "bytes_types",
"decodebytes",
"encodebytes",
- "bytestring",
- "byte_ord",
- "byte_chr",
- "byte_mask",
- "b",
- "u",
- "b2s",
- "StringIO",
- "BytesIO",
+ "input",
+ "integer_types",
"is_callable",
- "MAXSIZE",
+ "long",
"next",
- "builtins",
+ "string_types",
+ "text_type",
+ "u",
]
PY2 = sys.version_info[0] < 3
@@ -42,11 +41,6 @@ if PY2:
import __builtin__ as builtins
- def bytestring(s): # NOQA
- if isinstance(s, unicode): # NOQA
- return s.encode("utf-8")
- return s
-
byte_ord = ord # NOQA
byte_chr = chr # NOQA
@@ -124,9 +118,6 @@ else:
decodebytes = base64.decodebytes
encodebytes = base64.encodebytes
- def bytestring(s):
- return s
-
def byte_ord(c):
# In case we're handed a string instead of an int.
if not isinstance(c, int):
diff --git a/paramiko/sftp_client.py b/paramiko/sftp_client.py
index 1a9147fc..93190d85 100644
--- a/paramiko/sftp_client.py
+++ b/paramiko/sftp_client.py
@@ -28,7 +28,7 @@ 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
+from paramiko.py3compat import b, u, long
from paramiko.sftp import (
BaseSFTP,
CMD_OPENDIR,
@@ -522,7 +522,7 @@ class SFTPClient(BaseSFTP, ClosingContextManager):
"""
dest = self._adjust_cwd(dest)
self._log(DEBUG, "symlink({!r}, {!r})".format(source, dest))
- source = bytestring(source)
+ source = b(source)
self._request(CMD_SYMLINK, source, dest)
def chmod(self, path, mode):
diff --git a/paramiko/ssh_gss.py b/paramiko/ssh_gss.py
index 06aac761..a8424036 100644
--- a/paramiko/ssh_gss.py
+++ b/paramiko/ssh_gss.py
@@ -42,10 +42,6 @@ GSS_AUTH_AVAILABLE = True
GSS_EXCEPTIONS = ()
-from pyasn1.type.univ import ObjectIdentifier
-from pyasn1.codec.der import encoder, decoder
-
-
#: :var str _API: Constraint for the used API
_API = None
@@ -176,6 +172,9 @@ class _SSH_GSSAuth(object):
:note: In server mode we just return the OID length and the DER encoded
OID.
"""
+ from pyasn1.type.univ import ObjectIdentifier
+ from pyasn1.codec.der import encoder
+
OIDs = self._make_uint32(1)
krb5_OID = encoder.encode(ObjectIdentifier(self._krb5_mech))
OID_len = self._make_uint32(len(krb5_OID))
@@ -190,6 +189,8 @@ 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}
"""
+ from pyasn1.codec.der import decoder
+
mech, __ = decoder.decode(desired_mech)
if mech.__str__() != self._krb5_mech:
return False
@@ -283,6 +284,8 @@ class _SSH_GSSAPI_OLD(_SSH_GSSAuth):
:return: A ``String`` if the GSS-API has returned a token or
``None`` if no token was returned
"""
+ from pyasn1.codec.der import decoder
+
self._username = username
self._gss_host = target
targ_name = gssapi.Name(
@@ -635,6 +638,8 @@ class _SSH_SSPI(_SSH_GSSAuth):
:return: A ``String`` if the SSPI has returned a token or ``None`` if
no token was returned
"""
+ from pyasn1.codec.der import decoder
+
self._username = username
self._gss_host = target
error = 0
diff --git a/paramiko/util.py b/paramiko/util.py
index de4a5647..29c52bfb 100644
--- a/paramiko/util.py
+++ b/paramiko/util.py
@@ -245,16 +245,16 @@ def get_thread_id():
def log_to_file(filename, level=DEBUG):
"""send paramiko logs to a logfile,
if they're not already going somewhere"""
- l = logging.getLogger("paramiko")
- if len(l.handlers) > 0:
+ logger = logging.getLogger("paramiko")
+ if len(logger.handlers) > 0:
return
- l.setLevel(level)
+ logger.setLevel(level)
f = open(filename, "a")
- lh = logging.StreamHandler(f)
+ handler = logging.StreamHandler(f)
frm = "%(levelname)-.3s [%(asctime)s.%(msecs)03d] thr=%(_threadid)-3d"
frm += " %(name)s: %(message)s"
- lh.setFormatter(logging.Formatter(frm, "%Y%m%d-%H:%M:%S"))
- l.addHandler(lh)
+ handler.setFormatter(logging.Formatter(frm, "%Y%m%d-%H:%M:%S"))
+ logger.addHandler(handler)
# make only one filter object, so it doesn't get applied more than once
@@ -268,9 +268,9 @@ _pfilter = PFilter()
def get_logger(name):
- l = logging.getLogger(name)
- l.addFilter(_pfilter)
- return l
+ logger = logging.getLogger(name)
+ logger.addFilter(_pfilter)
+ return logger
def retry_on_signal(function):
diff --git a/setup.cfg b/setup.cfg
index bf86db42..7ff383c3 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -11,7 +11,9 @@ omit = paramiko/_winapi.py
exclude = sites,.git,build,dist,demos,tests
# NOTE: W503, E203 are concessions to black 18.0b5 and could be reinstated
# later if fixed on that end.
-ignore = E124,E125,E128,E261,E301,E302,E303,E402,E721,W503,E203
+# NOTE: E722 seems to only have started popping up on move to flake8 3.6.0 from
+# 2.4.0. Not sure why, bare excepts have been a (regrettable) thing forever...
+ignore = E124,E125,E128,E261,E301,E302,E303,E402,E721,W503,E203,E722
max-line-length = 79
[tool:pytest]
diff --git a/setup.py b/setup.py
index a427ccc3..e6c4a077 100644
--- a/setup.py
+++ b/setup.py
@@ -70,11 +70,8 @@ setup(
"Programming Language :: Python :: 3.4",
"Programming Language :: Python :: 3.5",
"Programming Language :: Python :: 3.6",
+ "Programming Language :: Python :: 3.7",
+ "Programming Language :: Python :: 3.8",
],
- install_requires=[
- "bcrypt>=3.1.3",
- "cryptography>=1.5",
- "pynacl>=1.0.1",
- "pyasn1>=0.1.7",
- ],
+ install_requires=["bcrypt>=3.1.3", "cryptography>=2.5", "pynacl>=1.0.1"],
)
diff --git a/sites/shared_conf.py b/sites/shared_conf.py
index f4806cf1..7bb503ce 100644
--- a/sites/shared_conf.py
+++ b/sites/shared_conf.py
@@ -14,6 +14,7 @@ html_theme_options = {
"github_repo": "paramiko",
"analytics_id": "UA-18486793-2",
"travis_button": True,
+ "tidelift_url": "https://tidelift.com/subscription/pkg/pypi-paramiko?utm_source=pypi-paramiko&utm_medium=referral&utm_campaign=docs",
}
html_sidebars = {
"**": ["about.html", "navigation.html", "searchbox.html", "donate.html"]
diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst
index 5c9843e8..032edb44 100644
--- a/sites/www/changelog.rst
+++ b/sites/www/changelog.rst
@@ -2,6 +2,29 @@
Changelog
=========
+- :support:`1379` (also :issue:`1369`) Raise Cryptography dependency
+ requirement to version 2.5 (from 1.5) and update some deprecated uses of its
+ API.
+
+ This removes a bunch of warnings of the style
+ ``CryptographyDeprecationWarning: encode_point has been deprecated on
+ EllipticCurvePublicNumbers and will be removed in a future version. Please
+ use EllipticCurvePublicKey.public_bytes to obtain both compressed and
+ uncompressed point encoding`` and similar, which users who had eventually
+ upgraded to Cryptography 2.x would encounter.
+
+ .. warning::
+ This change is backwards incompatible **if** you are unable to upgrade your
+ version of Cryptography. Please see `Cryptography's own changelog
+ <https://cryptography.io/en/latest/changelog/>`_ for details on what may
+ change when you upgrade; for the most part the only changes involved
+ dropping older Python versions (such as 2.6, 3.3, or some PyPy editions)
+ which Paramiko itself has already dropped.
+
+- :support:`1378 backported` Add support for the modern (as of Python 3.3)
+ import location of ``MutableMapping`` (used in host key management) to avoid
+ the old location becoming deprecated in Python 3.8. Thanks to Josh Karpel for
+ catch & patch.
- :release:`2.4.2 <2018-09-18>`
- :release:`2.3.3 <2018-09-18>`
- :release:`2.2.4 <2018-09-18>`
@@ -12,7 +35,7 @@ Changelog
behavior probably didn't cause any outright errors, but it doesn't seem to
conform to the RFCs and could cause (non-infinite) feedback loops in some
scenarios (usually those involving Paramiko on both ends).
-- :bug:`1283` Fix exploit (CVE pending) in Paramiko's server mode (**not**
+- :bug:`1283` Fix exploit (CVE-2018-1000805) in Paramiko's server mode (**not**
client mode) where hostile clients could trick the server into thinking they
were authenticated without actually submitting valid authentication.
@@ -44,6 +67,22 @@ Changelog
- :support:`1262 backported` Add ``*.pub`` files to the MANIFEST so distributed
source packages contain some necessary test assets. Credit: Alexander
Kapshuna.
+- :feature:`1212` Updated `SSHConfig.lookup <paramiko.config.SSHConfig.lookup>`
+ so it returns a new, type-casting-friendly dict subclass
+ (`~paramiko.config.SSHConfigDict`) in lieu of dict literals. This ought to be
+ backwards compatible, and allows an easier way to check boolean or int type
+ ``ssh_config`` values. Thanks to Chris Rose for the patch.
+- :support:`1191` Update our install docs with (somewhat) recently added
+ additional dependencies; we previously only required Cryptography, but the
+ docs never got updated after we incurred ``bcrypt`` and ``pynacl``
+ requirements for Ed25519 key support.
+
+ Additionally, ``pyasn1`` was never actually hard-required; it was necessary
+ during a development branch, and is used by the optional GSSAPI support, but
+ is not required for regular installation. Thus, it has been removed from our
+ ``setup.py`` and its imports in the GSSAPI code made optional.
+
+ Credit to ``@stevenwinfield`` for highlighting the outdated install docs.
- :release:`2.4.1 <2018-03-12>`
- :release:`2.3.2 <2018-03-12>`
- :release:`2.2.3 <2018-03-12>`
@@ -56,6 +95,10 @@ Changelog
where authentication status was not checked before processing channel-open
and other requests typically only sent after authenticating. Big thanks to
Matthijs Kooijman for the report.
+- :bug:`1168` Add newer key classes for Ed25519 and ECDSA to
+ ``paramiko.__all__`` so that code introspecting that attribute, or using
+ ``from paramiko import *`` (such as some IDEs) sees them. Thanks to
+ ``@patriksevallius`` for the patch.
- :bug:`1039` Ed25519 auth key decryption raised an unexpected exception when
given a unicode password string (typical in python 3). Report by Theodor van
Nahl and fix by Pierce Lopez.
@@ -75,7 +118,7 @@ Changelog
- :support:`1100` Updated the test suite & related docs/metadata/config to be
compatible with pytest instead of using the old, custom, crufty
unittest-based ``test.py``.
-
+
This includes marking known-slow tests (mostly the SFTP ones) so they can be
filtered out by ``inv test``'s default behavior; as well as other minor
tweaks to test collection and/or display (for example, GSSAPI tests are
diff --git a/sites/www/contact.rst b/sites/www/contact.rst
index 7e6c947e..dafc1bd4 100644
--- a/sites/www/contact.rst
+++ b/sites/www/contact.rst
@@ -6,7 +6,5 @@ You can get in touch with the developer & user community in any of the
following ways:
* IRC: ``#paramiko`` on Freenode
-* Mailing list: ``paramiko@librelist.com`` (see `the LibreList homepage
- <http://librelist.com>`_ for usage details).
* This website - a blog section is forthcoming.
* Submit contributions on Github - see the :doc:`contributing` page.
diff --git a/sites/www/installing-1.x.rst b/sites/www/installing-1.x.rst
index 8ede40d5..7421a6c2 100644
--- a/sites/www/installing-1.x.rst
+++ b/sites/www/installing-1.x.rst
@@ -118,4 +118,4 @@ First, see the main install doc's notes: :ref:`gssapi` - everything there is
required for Paramiko 1.x as well.
Additionally, users of Paramiko 1.x, on all platforms, need a final dependency:
-`pyasn1 <https://pypi.python.org/pypi/pyasn1>`_ ``0.1.7`` or better.
+`pyasn1 <https://pypi.org/project/pyasn1/>`_ ``0.1.7`` or better.
diff --git a/sites/www/installing.rst b/sites/www/installing.rst
index e6db2dca..3631eb0d 100644
--- a/sites/www/installing.rst
+++ b/sites/www/installing.rst
@@ -22,8 +22,12 @@ via `pip <http://pip-installer.org>`_::
We currently support **Python 2.7, 3.4+, and PyPy**. Users on Python 2.6 or
older (or 3.3 or older) are urged to upgrade.
-Paramiko has only one direct hard dependency: the Cryptography library. See
-:ref:`cryptography`.
+Paramiko has only a few direct dependencies:
+
+- The big one, with its own sub-dependencies, is Cryptography; see :ref:`its
+ specific note below <cryptography>` for more details.
+- `bcrypt <https://pypi.org/project/bcrypt/>`_, for Ed25519 key support;
+- `pynacl <https://pypi.org/project/PyNaCl/>`_, also for Ed25519 key support.
If you need GSS-API / SSPI support, see :ref:`the below subsection on it
<gssapi>` for details on its optional dependencies.
@@ -97,7 +101,7 @@ due to their infrequent utility & non-platform-agnostic requirements):
* It hopefully goes without saying but **all platforms** need **a working
installation of GSS-API itself**, e.g. Heimdal.
-* **Unix** needs `python-gssapi <https://pypi.python.org/pypi/python-gssapi/>`_
+* **Unix** needs `python-gssapi <https://pypi.org/project/python-gssapi/>`_
``0.6.1`` or better.
.. note:: This library appears to only function on Python 2.7 and up.
diff --git a/tests/test_auth.py b/tests/test_auth.py
index fe1a32a1..d98a00c4 100644
--- a/tests/test_auth.py
+++ b/tests/test_auth.py
@@ -142,7 +142,7 @@ class AuthTest(unittest.TestCase):
self.assertTrue(self.event.is_set())
self.assertTrue(self.ts.is_active())
- def test_1_bad_auth_type(self):
+ def test_bad_auth_type(self):
"""
verify that we get the right exception when an unsupported auth
type is requested.
@@ -160,7 +160,7 @@ class AuthTest(unittest.TestCase):
self.assertEqual(BadAuthenticationType, etype)
self.assertEqual(["publickey"], evalue.allowed_types)
- def test_2_bad_password(self):
+ def test_bad_password(self):
"""
verify that a bad password gets the right exception, and that a retry
with the right password works.
@@ -176,7 +176,7 @@ class AuthTest(unittest.TestCase):
self.tc.auth_password(username="slowdive", password="pygmalion")
self.verify_finished()
- def test_3_multipart_auth(self):
+ def test_multipart_auth(self):
"""
verify that multipart auth works.
"""
@@ -191,7 +191,7 @@ class AuthTest(unittest.TestCase):
self.assertEqual([], remain)
self.verify_finished()
- def test_4_interactive_auth(self):
+ def test_interactive_auth(self):
"""
verify keyboard-interactive auth works.
"""
@@ -210,7 +210,7 @@ class AuthTest(unittest.TestCase):
self.assertEqual([], remain)
self.verify_finished()
- def test_5_interactive_auth_fallback(self):
+ def test_interactive_auth_fallback(self):
"""
verify that a password auth attempt will fallback to "interactive"
if password auth isn't supported but interactive is.
@@ -221,7 +221,7 @@ class AuthTest(unittest.TestCase):
self.assertEqual([], remain)
self.verify_finished()
- def test_6_auth_utf8(self):
+ def test_auth_utf8(self):
"""
verify that utf-8 encoding happens in authentication.
"""
@@ -231,7 +231,7 @@ class AuthTest(unittest.TestCase):
self.assertEqual([], remain)
self.verify_finished()
- def test_7_auth_non_utf8(self):
+ def test_auth_non_utf8(self):
"""
verify that non-utf-8 encoded passwords can be used for broken
servers.
@@ -242,7 +242,7 @@ class AuthTest(unittest.TestCase):
self.assertEqual([], remain)
self.verify_finished()
- def test_8_auth_gets_disconnected(self):
+ def test_auth_gets_disconnected(self):
"""
verify that we catch a server disconnecting during auth, and report
it as an auth failure.
@@ -256,7 +256,7 @@ class AuthTest(unittest.TestCase):
self.assertTrue(issubclass(etype, AuthenticationException))
@slow
- def test_9_auth_non_responsive(self):
+ def test_auth_non_responsive(self):
"""
verify that authentication times out if server takes to long to
respond (or never responds).
diff --git a/tests/test_config.py b/tests/test_config.py
new file mode 100644
index 00000000..cbd3f623
--- /dev/null
+++ b/tests/test_config.py
@@ -0,0 +1,74 @@
+# This file is part of Paramiko and subject to the license in /LICENSE in this
+# repository
+
+import pytest
+
+from paramiko import config
+from paramiko.util import parse_ssh_config
+from paramiko.py3compat import StringIO
+
+
+def test_SSHConfigDict_construct_empty():
+ assert not config.SSHConfigDict()
+
+
+def test_SSHConfigDict_construct_from_list():
+ assert config.SSHConfigDict([(1, 2)])[1] == 2
+
+
+def test_SSHConfigDict_construct_from_dict():
+ assert config.SSHConfigDict({1: 2})[1] == 2
+
+
+@pytest.mark.parametrize("true_ish", ("yes", "YES", "Yes", True))
+def test_SSHConfigDict_as_bool_true_ish(true_ish):
+ assert config.SSHConfigDict({"key": true_ish}).as_bool("key") is True
+
+
+@pytest.mark.parametrize("false_ish", ("no", "NO", "No", False))
+def test_SSHConfigDict_as_bool(false_ish):
+ assert config.SSHConfigDict({"key": false_ish}).as_bool("key") is False
+
+
+@pytest.mark.parametrize("int_val", ("42", 42))
+def test_SSHConfigDict_as_int(int_val):
+ assert config.SSHConfigDict({"key": int_val}).as_int("key") == 42
+
+
+@pytest.mark.parametrize("non_int", ("not an int", None, object()))
+def test_SSHConfigDict_as_int_failures(non_int):
+ conf = config.SSHConfigDict({"key": non_int})
+
+ try:
+ int(non_int)
+ except Exception as e:
+ exception_type = type(e)
+
+ with pytest.raises(exception_type):
+ conf.as_int("key")
+
+
+def test_SSHConfig_host_dicts_are_SSHConfigDict_instances():
+ test_config_file = """
+Host *.example.com
+ Port 2222
+
+Host *
+ Port 3333
+ """
+ f = StringIO(test_config_file)
+ config = parse_ssh_config(f)
+ assert config.lookup("foo.example.com").as_int("port") == 2222
+
+
+def test_SSHConfig_wildcard_host_dicts_are_SSHConfigDict_instances():
+ test_config_file = """\
+Host *.example.com
+ Port 2222
+
+Host *
+ Port 3333
+ """
+ f = StringIO(test_config_file)
+ config = parse_ssh_config(f)
+ assert config.lookup("anything-else").as_int("port") == 3333
diff --git a/tests/test_kex.py b/tests/test_kex.py
index 62512beb..d42355a1 100644
--- a/tests/test_kex.py
+++ b/tests/test_kex.py
@@ -42,20 +42,20 @@ def dummy_urandom(n):
def dummy_generate_key_pair(obj):
private_key_value = 94761803665136558137557783047955027733968423115106677159790289642479432803037
public_key_numbers = "042bdab212fa8ba1b7c843301682a4db424d307246c7e1e6083c41d9ca7b098bf30b3d63e2ec6278488c135360456cc054b3444ecc45998c08894cbc1370f5f989"
- public_key_numbers_obj = ec.EllipticCurvePublicNumbers.from_encoded_point(
+ public_key_numbers_obj = ec.EllipticCurvePublicKey.from_encoded_point(
ec.SECP256R1(), unhexlify(public_key_numbers)
- )
+ ).public_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(
+ obj.Q_S = ec.EllipticCurvePublicKey.from_encoded_point(
ec.SECP256R1(), unhexlify(public_key_numbers)
- ).public_key(default_backend())
+ )
return
- obj.Q_C = ec.EllipticCurvePublicNumbers.from_encoded_point(
+ obj.Q_C = ec.EllipticCurvePublicKey.from_encoded_point(
ec.SECP256R1(), unhexlify(public_key_numbers)
- ).public_key(default_backend())
+ )
class FakeKey(object):