summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJeff Forcier <jeff@bitprophet.org>2019-08-27 14:20:27 -0400
committerJeff Forcier <jeff@bitprophet.org>2019-09-26 13:51:01 -0500
commit8cd97c0b1df8a6902c3c77d18282f604287caca9 (patch)
tree616f5f56f54b517c9d6f9b687c4f91e0b95ffa04
parentb1bbacdcc4f0be50b8fe584f329d344fb13544bd (diff)
downloadparamiko-897-canonicalization.tar.gz
Implement ssh_config hostname canonicalization (WIP)897-canonicalization
- Refactor DNS lookup related junk previously only relevant to %h - Refactor guts of lookup() so it can be done >1 time - Changelog/tests/implementation for canonicalization itself Closes #897
-rw-r--r--paramiko/__init__.py2
-rw-r--r--paramiko/config.py151
-rw-r--r--paramiko/ssh_exception.py8
-rw-r--r--sites/docs/api/config.rst22
-rw-r--r--sites/www/changelog.rst5
-rw-r--r--tests/configs/basic.config6
-rw-r--r--tests/configs/canon-always.config8
-rw-r--r--tests/configs/canon-ipv4.config9
-rw-r--r--tests/configs/canon-local-always.config9
-rw-r--r--tests/configs/canon-local.config9
-rw-r--r--tests/configs/canon.config8
-rw-r--r--tests/configs/deep-canon-maxdots.config14
-rw-r--r--tests/configs/deep-canon.config13
-rw-r--r--tests/configs/empty-canon.config9
-rw-r--r--tests/configs/fallback-no.config9
-rw-r--r--tests/configs/fallback-yes.config8
-rw-r--r--tests/configs/multi-canon-domains.config8
-rw-r--r--tests/configs/no-canon.config8
-rw-r--r--tests/configs/robey.config (renamed from tests/robey.config)0
-rw-r--r--tests/configs/zero-maxdots.config11
-rw-r--r--tests/test_config.py190
-rw-r--r--tests/test_util.py73
-rw-r--r--tests/util.py9
23 files changed, 512 insertions, 77 deletions
diff --git a/paramiko/__init__.py b/paramiko/__init__.py
index 6bd8b38f..d8e60bb2 100644
--- a/paramiko/__init__.py
+++ b/paramiko/__init__.py
@@ -40,6 +40,7 @@ from paramiko.ssh_exception import (
BadAuthenticationType,
BadHostKeyException,
ChannelException,
+ CouldNotCanonicalize,
PasswordRequiredException,
ProxyCommandFailure,
SSHException,
@@ -104,6 +105,7 @@ __all__ = [
"BufferedFile",
"Channel",
"ChannelException",
+ "CouldNotCanonicalize",
"DSSKey",
"ECDSAKey",
"Ed25519Key",
diff --git a/paramiko/config.py b/paramiko/config.py
index f9ea02dc..d00d5f38 100644
--- a/paramiko/config.py
+++ b/paramiko/config.py
@@ -29,6 +29,8 @@ import socket
from .py3compat import StringIO
+from .ssh_exception import CouldNotCanonicalize
+
SSH_PORT = 22
@@ -170,29 +172,92 @@ class SSHConfig(object):
.. versionchanged:: 2.5
Returns `SSHConfigDict` objects instead of dict literals.
+ .. versionchanged:: 2.7
+ Added canonicalization support.
"""
+ # First pass
+ options = self._lookup(hostname=hostname)
+ # Handle canonicalization
+ canon = options.get("canonicalizehostname", None) in ("yes", "always")
+ # TODO: should we be doing the int-ifying lower down?
+ maxdots = int(options.get("canonicalizemaxdots", 1))
+ if canon and hostname.count(".") <= maxdots:
+ # NOTE: OpenSSH manpage does not explicitly state this, but its
+ # implementation for CanonicalDomains is 'split on any whitespace'.
+ domains = options["canonicaldomains"].split()
+ # TODO: graceful error (or silent no-op?) if domains option not
+ # found at all
+ hostname = self.canonicalize(hostname, options, domains)
+ options["hostname"] = hostname
+ options = self._lookup(hostname, options)
+ # TODO: so does a HostName found therein overwrite ours or what
+ return options
+
+ def _lookup(self, hostname, options=None):
matches = [
config
for config in self._config
if self._allowed(config["host"], hostname)
]
- ret = SSHConfigDict()
+ if options is None:
+ options = SSHConfigDict()
for match in matches:
for key, value in match["config"].items():
- if key not in ret:
+ if key not in options:
# Create a copy of the original value,
# else it will reference the original list
# in self._config and update that value too
# when the extend() is being called.
- ret[key] = value[:] if value is not None else value
+ options[key] = value[:] if value is not None else value
elif key == "identityfile":
- ret[key].extend(value)
- ret = self._expand_variables(ret, hostname)
+ options[key].extend(value)
+ options = self._expand_variables(options, hostname)
# TODO: remove in 3.x re #670
- if "proxycommand" in ret and ret["proxycommand"] is None:
- del ret["proxycommand"]
- return ret
+ if "proxycommand" in options and options["proxycommand"] is None:
+ del options["proxycommand"]
+ return options
+
+ def canonicalize(self, hostname, options, domains):
+ """
+ Return canonicalized version of ``hostname``.
+
+ :param str hostname: Target hostname.
+ :param options: An `SSHConfigDict` from a previous lookup pass.
+ :param list domains: List of domains (e.g. ``["paramiko.org"]``).
+
+ :returns: A canonicalized hostname if one was found, else ``None``.
+
+ .. versionadded:: 2.7
+ """
+ found = False
+ for domain in domains:
+ candidate = "{}.{}".format(hostname, domain)
+ family_specific = _addressfamily_host_lookup(candidate, options)
+ if family_specific is not None:
+ # TODO: would we want to dig deeper into other results? e.g. to
+ # find something that satisfies PermittedCNAMEs when that is
+ # implemented?
+ found = family_specific[0]
+ else:
+ # TODO: what does ssh use here and is there a reason to use
+ # that instead of gethostbyname?
+ try:
+ found = socket.gethostbyname(candidate)
+ except socket.gaierror:
+ pass
+ if found:
+ # TODO: follow CNAME (implied by found != candidate?) if
+ # CanonicalizePermittedCNAMEs allows it
+ return candidate
+ # If we got here, it means canonicalization failed.
+ # When CanonicalizeFallbackLocal is undefined or 'yes', we just spit
+ # back the original hostname.
+ if options.get("canonicalizefallbacklocal", "yes") == "yes":
+ return hostname
+ # And here, we failed AND fallback was set to a non-yes value, so we
+ # need to get mad.
+ raise CouldNotCanonicalize(hostname)
def get_hostnames(self):
"""
@@ -296,6 +361,43 @@ class SSHConfig(object):
raise Exception("Unparsable host {}".format(host))
+def _addressfamily_host_lookup(hostname, options):
+ """
+ Try looking up ``hostname`` in an IPv4 or IPv6 specific manner.
+
+ This is an odd duck due to needing use in two divergent use cases. It looks
+ up ``AddressFamily`` in ``options`` and if it is ``inet`` or ``inet6``,
+ this function uses `socket.getaddrinfo` to perform a family-specific
+ lookup, returning the result if successful.
+
+ In any other situation -- lookup failure, or ``AddressFamily`` being
+ unspecified or ``any`` -- ``None`` is returned instead and the caller is
+ expected to do something situation-appropriate like calling
+ `socket.gethostbyname`.
+
+ :param str hostname: Hostname to look up.
+ :param options: `SSHConfigDict` instance w/ parsed options.
+ :returns: ``getaddrinfo``-style tuples, or ``None``, depending.
+ """
+ address_family = options.get("addressfamily", "any").lower()
+ if address_family == "any":
+ return
+ try:
+ family = socket.AF_INET6
+ if address_family == "inet":
+ family = socket.AF_INET
+ return socket.getaddrinfo(
+ hostname,
+ None,
+ family,
+ socket.SOCK_DGRAM,
+ socket.IPPROTO_IP,
+ socket.AI_CANONNAME,
+ )
+ except socket.gaierror:
+ pass
+
+
class LazyFqdn(object):
"""
Returns the host's fqdn on request as string.
@@ -319,31 +421,14 @@ class LazyFqdn(object):
# Handle specific option
fqdn = None
- address_family = self.config.get("addressfamily", "any").lower()
- if address_family != "any":
- try:
- family = socket.AF_INET6
- if address_family == "inet":
- socket.AF_INET
- results = socket.getaddrinfo(
- self.host,
- None,
- family,
- socket.SOCK_DGRAM,
- socket.IPPROTO_IP,
- socket.AI_CANONNAME,
- )
- for res in results:
- af, socktype, proto, canonname, sa = res
- if canonname and "." in canonname:
- fqdn = canonname
- break
- # giaerror -> socket.getaddrinfo() can't resolve self.host
- # (which is from socket.gethostname()). Fall back to the
- # getfqdn() call below.
- except socket.gaierror:
- pass
- # Handle 'any' / unspecified
+ results = _addressfamily_host_lookup(self.host, self.config)
+ if results is not None:
+ for res in results:
+ af, socktype, proto, canonname, sa = res
+ if canonname and "." in canonname:
+ fqdn = canonname
+ break
+ # Handle 'any' / unspecified / lookup failure
if fqdn is None:
fqdn = socket.getfqdn()
# Cache
diff --git a/paramiko/ssh_exception.py b/paramiko/ssh_exception.py
index b525468a..a8ec4cdd 100644
--- a/paramiko/ssh_exception.py
+++ b/paramiko/ssh_exception.py
@@ -196,3 +196,11 @@ class NoValidConnectionsError(socket.error):
def __reduce__(self):
return (self.__class__, (self.errors,))
+
+
+class CouldNotCanonicalize(SSHException):
+ """
+ Raised when hostname canonicalization fails & fallback is disabled.
+ """
+
+ pass
diff --git a/sites/docs/api/config.rst b/sites/docs/api/config.rst
index e402dd5e..8c17df97 100644
--- a/sites/docs/api/config.rst
+++ b/sites/docs/api/config.rst
@@ -35,9 +35,27 @@ Paramiko versions lacking some default parse-related behavior.
See `OpenSSH's own ssh_config docs <ssh_config>`_ for details on the overall
file format, and the intended meaning of the keywords and values; or check the
-documentation for your Paramiko-using library of choice (again, often
-`Fabric`_) to see what it honors on its end.
+documentation for your Paramiko-using library of choice (e.g. `Fabric`_) to see
+what it honors on its end.
+
+- ``CanonicalDomains``: sets the domains used for hostname canonicalization.
+- ``CanonicalizeFallbackLocal``: set to ``no`` to enforce that all looked-up
+ names must resolve under one of the ``CanonicalDomains`` - any names which
+ don't canonicalize will raise `CouldNotCanonicalize` (instead of silently
+ returning a config containing only global-level config values, as normal).
+- ``CanonicalizeHostname``: as with OpenSSH, when a lookup results in this
+ being set to ``yes`` (whether globally or inside a specific block), it
+ triggers an attempt to resolve the requested hostname under one of the given
+ ``CanonicalDomains``, which if successful will cause Paramiko to re-parse the
+ entire config file.
+ .. note::
+ As in OpenSSH, canonicalization is quietly ignored for "deep" hostnames -
+ by default, hostnames containing more than one period character. This may
+ be controlled with ``CanonicalizeMaxDots``; see below.
+
+- ``CanonicalizeMaxDots``: controls how many period characters may appear in a
+ target hostname before canonicalization is disabled.
- ``AddressFamily``: used when looking up the local hostname for purposes of
expanding the ``%l``/``%L`` :ref:`tokens <TOKENS>`.
diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst
index 63f64e50..cfd81a5d 100644
--- a/sites/www/changelog.rst
+++ b/sites/www/changelog.rst
@@ -2,6 +2,11 @@
Changelog
=========
+- :feature:`897` Implement most 'canonical hostname' ``ssh_config``
+ functionality (``CanonicalizeHostname``, ``CanonicalDomains``,
+ ``CanonicalizeFallbackLocal``, and ``CanonicalizeMaxDots``;
+ ``CanonicalizePermittedCNAMEs`` has **not** yet been implemented). All were
+ previously silently ignored. Reported by Michael Leinartas.
- :support:`-` Explicitly document :ref:`which ssh_config features we
currently support <ssh-config-support>`. Previously users just had to guess,
which is simply no good.
diff --git a/tests/configs/basic.config b/tests/configs/basic.config
new file mode 100644
index 00000000..1ae37cc6
--- /dev/null
+++ b/tests/configs/basic.config
@@ -0,0 +1,6 @@
+CanonicalDomains paramiko.org
+
+Host www.paramiko.org
+ User rando
+
+# vim: set ft=sshconfig :
diff --git a/tests/configs/canon-always.config b/tests/configs/canon-always.config
new file mode 100644
index 00000000..85058a14
--- /dev/null
+++ b/tests/configs/canon-always.config
@@ -0,0 +1,8 @@
+CanonicalDomains paramiko.org
+CanonicalizeHostname always
+
+Host www.paramiko.org
+ User rando
+
+
+# vim: set ft=sshconfig :
diff --git a/tests/configs/canon-ipv4.config b/tests/configs/canon-ipv4.config
new file mode 100644
index 00000000..9f48273e
--- /dev/null
+++ b/tests/configs/canon-ipv4.config
@@ -0,0 +1,9 @@
+CanonicalDomains paramiko.org
+CanonicalizeHostname yes
+AddressFamily inet
+
+Host www.paramiko.org
+ User rando
+
+
+# vim: set ft=sshconfig :
diff --git a/tests/configs/canon-local-always.config b/tests/configs/canon-local-always.config
new file mode 100644
index 00000000..c821d113
--- /dev/null
+++ b/tests/configs/canon-local-always.config
@@ -0,0 +1,9 @@
+Host www.paramiko.org
+ User rando
+
+Host www
+ CanonicalDomains paramiko.org
+ CanonicalizeHostname always
+
+
+# vim: set ft=sshconfig :
diff --git a/tests/configs/canon-local.config b/tests/configs/canon-local.config
new file mode 100644
index 00000000..418f7723
--- /dev/null
+++ b/tests/configs/canon-local.config
@@ -0,0 +1,9 @@
+Host www.paramiko.org
+ User rando
+
+Host www
+ CanonicalDomains paramiko.org
+ CanonicalizeHostname yes
+
+
+# vim: set ft=sshconfig :
diff --git a/tests/configs/canon.config b/tests/configs/canon.config
new file mode 100644
index 00000000..e5c732e4
--- /dev/null
+++ b/tests/configs/canon.config
@@ -0,0 +1,8 @@
+CanonicalizeHostname yes
+CanonicalDomains paramiko.org
+
+Host www.paramiko.org
+ User rando
+
+
+# vim: set ft=sshconfig :
diff --git a/tests/configs/deep-canon-maxdots.config b/tests/configs/deep-canon-maxdots.config
new file mode 100644
index 00000000..37a82e72
--- /dev/null
+++ b/tests/configs/deep-canon-maxdots.config
@@ -0,0 +1,14 @@
+CanonicalizeHostname yes
+CanonicalDomains paramiko.org
+CanonicalizeMaxDots 2
+
+Host www.paramiko.org
+ User rando
+
+Host sub.www.paramiko.org
+ User deep
+
+Host subber.sub.www.paramiko.org
+ User deeper
+
+# vim: set ft=sshconfig :
diff --git a/tests/configs/deep-canon.config b/tests/configs/deep-canon.config
new file mode 100644
index 00000000..3c111f48
--- /dev/null
+++ b/tests/configs/deep-canon.config
@@ -0,0 +1,13 @@
+CanonicalizeHostname yes
+CanonicalDomains paramiko.org
+
+Host www.paramiko.org
+ User rando
+
+Host sub.www.paramiko.org
+ User deep
+
+Host subber.sub.www.paramiko.org
+ User deeper
+
+# vim: set ft=sshconfig :
diff --git a/tests/configs/empty-canon.config b/tests/configs/empty-canon.config
new file mode 100644
index 00000000..f268a2ca
--- /dev/null
+++ b/tests/configs/empty-canon.config
@@ -0,0 +1,9 @@
+CanonicalizeHostname yes
+CanonicalDomains
+AddressFamily inet
+
+Host www.paramiko.org
+ User rando
+
+
+# vim: set ft=sshconfig :
diff --git a/tests/configs/fallback-no.config b/tests/configs/fallback-no.config
new file mode 100644
index 00000000..86b6a484
--- /dev/null
+++ b/tests/configs/fallback-no.config
@@ -0,0 +1,9 @@
+CanonicalizeHostname yes
+CanonicalDomains paramiko.org
+CanonicalizeFallbackLocal no
+
+Host www.paramiko.org
+ User rando
+
+
+# vim: set ft=sshconfig :
diff --git a/tests/configs/fallback-yes.config b/tests/configs/fallback-yes.config
new file mode 100644
index 00000000..a07064a0
--- /dev/null
+++ b/tests/configs/fallback-yes.config
@@ -0,0 +1,8 @@
+CanonicalizeHostname yes
+CanonicalDomains paramiko.org
+CanonicalizeFallbackLocal yes
+
+Host www.paramiko.org
+ User rando
+
+# vim: set ft=sshconfig :
diff --git a/tests/configs/multi-canon-domains.config b/tests/configs/multi-canon-domains.config
new file mode 100644
index 00000000..f0cf521d
--- /dev/null
+++ b/tests/configs/multi-canon-domains.config
@@ -0,0 +1,8 @@
+CanonicalizeHostname yes
+CanonicalDomains not-a-real-tld paramiko.org
+
+Host www.paramiko.org
+ User rando
+
+
+# vim: set ft=sshconfig :
diff --git a/tests/configs/no-canon.config b/tests/configs/no-canon.config
new file mode 100644
index 00000000..bd48b790
--- /dev/null
+++ b/tests/configs/no-canon.config
@@ -0,0 +1,8 @@
+CanonicalizeHostname no
+CanonicalDomains paramiko.org
+
+Host www.paramiko.org
+ User rando
+
+
+# vim: set ft=sshconfig :
diff --git a/tests/robey.config b/tests/configs/robey.config
index 2175182f..2175182f 100644
--- a/tests/robey.config
+++ b/tests/configs/robey.config
diff --git a/tests/configs/zero-maxdots.config b/tests/configs/zero-maxdots.config
new file mode 100644
index 00000000..c7a095ab
--- /dev/null
+++ b/tests/configs/zero-maxdots.config
@@ -0,0 +1,11 @@
+CanonicalizeHostname yes
+CanonicalDomains paramiko.org
+CanonicalizeMaxDots 0
+
+Host www.paramiko.org
+ User rando
+
+Host sub.www.paramiko.org
+ User deep
+
+# vim: set ft=sshconfig :
diff --git a/tests/test_config.py b/tests/test_config.py
index 57dda537..f930a6d3 100644
--- a/tests/test_config.py
+++ b/tests/test_config.py
@@ -2,18 +2,24 @@
# repository
from os.path import expanduser
+from socket import gaierror
-from pytest import raises, mark
+from mock import patch
+from pytest import raises, mark, skip, fixture
-from paramiko import SSHConfig, SSHConfigDict
+from paramiko import SSHConfig, SSHConfigDict, CouldNotCanonicalize
from paramiko.util import lookup_ssh_host_config
-from .util import _support
+from .util import _config
+
+
+def load_config(name):
+ return SSHConfig.from_path(_config(name))
class TestSSHConfig(object):
def setup(self):
- self.config = SSHConfig.from_path(_support("robey.config"))
+ self.config = load_config("robey")
def test_init(self):
# No args!
@@ -27,12 +33,13 @@ class TestSSHConfig(object):
assert config.lookup("foo.example.com")["user"] == "foo"
def test_from_file(self):
- with open(_support("robey.config")) as flo:
+ with open(_config("robey")) as flo:
config = SSHConfig.from_file(flo)
assert config.lookup("whatever")["user"] == "robey"
def test_from_path(self):
- config = SSHConfig.from_path(_support("robey.config"))
+ # NOTE: DO NOT replace with use of load_config() :D
+ config = SSHConfig.from_path(_config("robey"))
assert config.lookup("meh.example.com")["port"] == "3333"
def test_parse_config(self):
@@ -451,3 +458,174 @@ Host *
"""
)
assert config.lookup("anything-else").as_int("port") == 3333
+
+
+@fixture
+def socket():
+ with patch("paramiko.config.socket") as mocket:
+ # Reinstate gaierror as an actual exception and not a sub-mock.
+ # (Presumably this would work with any exception, but why not use the
+ # real one?)
+ mocket.gaierror = gaierror
+ # Patch out getaddrinfo, used to detect family-specific IP lookup -
+ # only useful for a few specific tests.
+ mocket.getaddrinfo.side_effect = mocket.gaierror
+ yield mocket
+
+
+class TestHostnameCanonicalization(object):
+ # NOTE: this class uses on-disk configs, and ones with real (at time of
+ # writing) DNS names, so that one can easily test OpenSSH's behavior using
+ # "ssh -F path/to/file.config -G <target>".
+
+ def test_off_by_default(self, socket):
+ result = load_config("basic").lookup("www")
+ assert result["hostname"] == "www"
+ assert "user" not in result
+ assert not socket.gethostbyname.called
+
+ def test_explicit_no_same_as_default(self, socket):
+ result = load_config("no-canon").lookup("www")
+ assert result["hostname"] == "www"
+ assert "user" not in result
+ assert not socket.gethostbyname.called
+
+ @mark.parametrize(
+ "config_name",
+ ("canon", "canon-always", "canon-local", "canon-local-always"),
+ )
+ def test_canonicalization_base_cases(self, socket, config_name):
+ result = load_config(config_name).lookup("www")
+ assert result["hostname"] == "www.paramiko.org"
+ assert result["user"] == "rando"
+ socket.gethostbyname.assert_called_once_with("www.paramiko.org")
+
+ def test_uses_getaddrinfo_when_AddressFamily_given(self, socket):
+ # Undo default 'always fails' mock
+ socket.getaddrinfo.side_effect = None
+ socket.getaddrinfo.return_value = [True] # just need 1st value truthy
+ result = load_config("canon-ipv4").lookup("www")
+ assert result["hostname"] == "www.paramiko.org"
+ assert result["user"] == "rando"
+ assert not socket.gethostbyname.called
+ gai_args = socket.getaddrinfo.call_args[0]
+ assert gai_args[0] == "www.paramiko.org"
+ assert gai_args[2] is socket.AF_INET # Mocked, but, still useful
+
+ @mark.skip
+ def test_empty_CanonicalDomains_canonicalizes_despite_noop(self, socket):
+ # Confirmed this is how OpenSSH behaves as well. Bit silly, but.
+ # TODO: this requires modifying SETTINGS_REGEX, which is a mite scary
+ # (honestly I'd prefer to move to a real parser lib anyhow) and since
+ # this is a very dumb corner case, it's marked skip for now.
+ result = load_config("empty-canon").lookup("www")
+ assert result["hostname"] == "www" # no paramiko.org
+ assert "user" not in result # did not discover canonicalized block
+
+ def test_CanonicalDomains_may_be_set_to_space_separated_list(self, socket):
+ # Test config has a bogus domain, followed by paramiko.org
+ socket.gethostbyname.side_effect = [socket.gaierror, True]
+ result = load_config("multi-canon-domains").lookup("www")
+ assert result["hostname"] == "www.paramiko.org"
+ assert result["user"] == "rando"
+ assert [x[0][0] for x in socket.gethostbyname.call_args_list] == [
+ "www.not-a-real-tld",
+ "www.paramiko.org",
+ ]
+
+ def test_canonicalization_applies_to_single_dot_by_default(self, socket):
+ result = load_config("deep-canon").lookup("sub.www")
+ assert result["hostname"] == "sub.www.paramiko.org"
+ assert result["user"] == "deep"
+
+ def test_canonicalization_not_applied_to_two_dots_by_default(self, socket):
+ result = load_config("deep-canon").lookup("subber.sub.www")
+ assert result["hostname"] == "subber.sub.www"
+ assert "user" not in result
+
+ def test_hostname_depth_controllable_with_max_dots_directive(self, socket):
+ # This config sets MaxDots of 2, so now canonicalization occurs
+ result = load_config("deep-canon-maxdots").lookup("subber.sub.www")
+ assert result["hostname"] == "subber.sub.www.paramiko.org"
+ assert result["user"] == "deeper"
+
+ def test_max_dots_may_be_zero(self, socket):
+ result = load_config("zero-maxdots").lookup("sub.www")
+ assert result["hostname"] == "sub.www"
+ assert "user" not in result
+
+ def test_reparsing_does_not_occur_when_canonicalization_fails(
+ self, socket
+ ):
+ # TODO: what if the given name doesn't even resolve when canonicalized
+ # _and_ CanonicalizeFallbackLocal is not active? Do we fail or just
+ # return the first parse phase result?
+ skip()
+
+ def test_ProxyCommand_not_canonicalized_when_canonical_yes(self, socket):
+ # TODO: may be only applicable at Fabric level?
+ skip()
+
+ def test_ProxyJump_not_canonicalized_when_canonical_yes(self, socket):
+ # TODO: may be only applicable at Fabric level?
+ skip()
+
+ def test_ProxyCommand_canonicalized_when_canonical_always(self, socket):
+ # TODO: may be only applicable at Fabric level?
+ skip()
+
+ def test_ProxyJump_canonicalized_when_canonical_always(self, socket):
+ # TODO: may be only applicable at Fabric level?
+ skip()
+
+ def test_fallback_yes_does_not_canonicalize_or_error(self, socket):
+ socket.gethostbyname.side_effect = socket.gaierror
+ result = load_config("fallback-yes").lookup("www")
+ assert result["hostname"] == "www"
+ assert "user" not in result
+
+ def test_fallback_no_causes_errors_for_unresolvable_names(self, socket):
+ socket.gethostbyname.side_effect = socket.gaierror
+ with raises(CouldNotCanonicalize) as info:
+ load_config("fallback-no").lookup("doesnotexist")
+ assert str(info.value) == "doesnotexist"
+
+ def test_identityfile_continues_being_appended_to(self, socket):
+ # TODO: identityfile loaded in first pass, then appended to in
+ # canonicalized pass
+ skip()
+
+ def test_variable_expansion_of_hostname_applies_in_right_order(
+ self, socket
+ ):
+ # TODO: make sure we match OpenSSH behavior here, including corner
+ # cases (e.g. who wins, the canonicalized version of the hostname or an
+ # explicit HostName that newly matches after canonicalization? XD)
+ skip()
+
+
+@mark.skip
+class TestCanonicalizationOfCNAMEs(object):
+ def test_permitted_cnames_may_be_one_to_one_mapping(self):
+ # CanonicalizePermittedCNAMEs *.foo.com:*.bar.com
+ pass
+
+ def test_permitted_cnames_may_be_one_to_many_mapping(self):
+ # CanonicalizePermittedCNAMEs *.foo.com:*.bar.com,*.biz.com
+ pass
+
+ def test_permitted_cnames_may_be_many_to_one_mapping(self):
+ # CanonicalizePermittedCNAMEs *.foo.com,*.bar.com:*.biz.com
+ pass
+
+ def test_permitted_cnames_may_be_many_to_many_mapping(self):
+ # CanonicalizePermittedCNAMEs *.foo.com,*.bar.com:*.biz.com,*.baz.com
+ pass
+
+ def test_permitted_cnames_may_be_multiple_mappings(self):
+ # CanonicalizePermittedCNAMEs *.foo.com,*.bar.com *.biz.com:*.baz.com
+ pass
+
+ def test_permitted_cnames_may_be_multiple_complex_mappings(self):
+ # Same as prev but with multiple patterns on both ends in both args
+ pass
diff --git a/tests/test_util.py b/tests/test_util.py
index 7849fec3..84a48bd3 100644
--- a/tests/test_util.py
+++ b/tests/test_util.py
@@ -43,44 +43,47 @@ BGQ3GQ/Fc7SX6gkpXkwcZryoi4kNFhHu5LvHcZPdxXV1D+uTMfGS1eyd2Yz/DoNWXNAl8TI0cAsW\
class UtilTest(unittest.TestCase):
- def test_import(self):
+ def test_imports(self):
"""
verify that all the classes can be imported from paramiko.
"""
- symbols = paramiko.__all__
- self.assertTrue("Agent" in symbols)
- self.assertTrue("AgentKey" in symbols)
- self.assertTrue("AuthenticationException" in symbols)
- self.assertTrue("AutoAddPolicy" in symbols)
- self.assertTrue("BadAuthenticationType" in symbols)
- self.assertTrue("BufferedFile" in symbols)
- self.assertTrue("Channel" in symbols)
- self.assertTrue("ChannelException" in symbols)
- self.assertTrue("DSSKey" in symbols)
- self.assertTrue("HostKeys" in symbols)
- self.assertTrue("Message" in symbols)
- self.assertTrue("MissingHostKeyPolicy" in symbols)
- self.assertTrue("PasswordRequiredException" in symbols)
- self.assertTrue("RSAKey" in symbols)
- self.assertTrue("RejectPolicy" in symbols)
- self.assertTrue("SFTP" in symbols)
- self.assertTrue("SFTPAttributes" in symbols)
- self.assertTrue("SFTPClient" in symbols)
- self.assertTrue("SFTPError" in symbols)
- self.assertTrue("SFTPFile" in symbols)
- self.assertTrue("SFTPHandle" in symbols)
- self.assertTrue("SFTPServer" in symbols)
- self.assertTrue("SFTPServerInterface" in symbols)
- self.assertTrue("SSHClient" in symbols)
- self.assertTrue("SSHConfig" in symbols)
- self.assertTrue("SSHConfigDict" in symbols)
- self.assertTrue("SSHException" in symbols)
- self.assertTrue("SecurityOptions" in symbols)
- self.assertTrue("ServerInterface" in symbols)
- self.assertTrue("SubsystemHandler" in symbols)
- self.assertTrue("Transport" in symbols)
- self.assertTrue("WarningPolicy" in symbols)
- self.assertTrue("util" in symbols)
+ for name in (
+ "Agent",
+ "AgentKey",
+ "AuthenticationException",
+ "AutoAddPolicy",
+ "BadAuthenticationType",
+ "BufferedFile",
+ "Channel",
+ "ChannelException",
+ "CouldNotCanonicalize",
+ "DSSKey",
+ "HostKeys",
+ "Message",
+ "MissingHostKeyPolicy",
+ "PasswordRequiredException",
+ "RSAKey",
+ "RejectPolicy",
+ "SFTP",
+ "SFTPAttributes",
+ "SFTPClient",
+ "SFTPError",
+ "SFTPFile",
+ "SFTPHandle",
+ "SFTPServer",
+ "SFTPServerInterface",
+ "SSHClient",
+ "SSHConfig",
+ "SSHConfigDict",
+ "SSHException",
+ "SecurityOptions",
+ "ServerInterface",
+ "SubsystemHandler",
+ "Transport",
+ "WarningPolicy",
+ "util",
+ ):
+ assert name in paramiko.__all__
def test_generate_key_bytes(self):
x = paramiko.util.generate_key_bytes(
diff --git a/tests/util.py b/tests/util.py
index cdc835c9..339677aa 100644
--- a/tests/util.py
+++ b/tests/util.py
@@ -9,8 +9,15 @@ from paramiko.py3compat import builtins
from paramiko.ssh_gss import GSS_AUTH_AVAILABLE
+tests_dir = dirname(realpath(__file__))
+
+
def _support(filename):
- return join(dirname(realpath(__file__)), filename)
+ return join(tests_dir, filename)
+
+
+def _config(name):
+ return join(tests_dir, "configs", "{}.config".format(name))
needs_gssapi = pytest.mark.skipif(