summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--README.rst2
-rw-r--r--paramiko/__init__.py2
-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/py3compat.py41
-rw-r--r--paramiko/sftp_client.py4
-rw-r--r--paramiko/ssh_gss.py13
-rw-r--r--setup.py7
-rw-r--r--sites/www/changelog.rst20
-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
15 files changed, 250 insertions, 67 deletions
diff --git a/README.rst b/README.rst
index 6e49bd68..ab383459 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-2018 Jeff Forcier <jeff@bitprophet.org>
:License: `LGPL <https://www.gnu.org/copyleft/lesser.html>`_
:Homepage: http://www.paramiko.org/
:API docs: http://docs.paramiko.org
diff --git a/paramiko/__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/auth_handler.py b/paramiko/auth_handler.py
index 3f0456e5..41724832 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/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 eb8826e0..3f299aee 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 = "MIT"
@@ -163,6 +159,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))
@@ -177,6 +176,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
@@ -269,6 +270,8 @@ class _SSH_GSSAPI(_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(
@@ -443,6 +446,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/setup.py b/setup.py
index a427ccc3..c8a0169c 100644
--- a/setup.py
+++ b/setup.py
@@ -71,10 +71,5 @@ setup(
"Programming Language :: Python :: 3.5",
"Programming Language :: Python :: 3.6",
],
- install_requires=[
- "bcrypt>=3.1.3",
- "cryptography>=1.5",
- "pynacl>=1.0.1",
- "pyasn1>=0.1.7",
- ],
+ install_requires=["bcrypt>=3.1.3", "cryptography>=1.5", "pynacl>=1.0.1"],
)
diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst
index 8f612bd1..53a484b9 100644
--- a/sites/www/changelog.rst
+++ b/sites/www/changelog.rst
@@ -5,6 +5,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>`
@@ -17,6 +33,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.
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