diff options
author | Jeff Forcier <jeff@bitprophet.org> | 2019-08-27 14:20:27 -0400 |
---|---|---|
committer | Jeff Forcier <jeff@bitprophet.org> | 2019-09-27 14:17:36 -0500 |
commit | 4c4de253e3909adb99505b6723c58c23d64f7988 (patch) | |
tree | f14352dbfc135fd781360f0a909342079a82f9c1 | |
parent | b1bbacdcc4f0be50b8fe584f329d344fb13544bd (diff) | |
download | paramiko-4c4de253e3909adb99505b6723c58c23d64f7988.tar.gz |
Implement ssh_config hostname canonicalization (WIP)
- 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__.py | 2 | ||||
-rw-r--r-- | paramiko/config.py | 149 | ||||
-rw-r--r-- | paramiko/ssh_exception.py | 8 | ||||
-rw-r--r-- | sites/docs/api/config.rst | 22 | ||||
-rw-r--r-- | sites/www/changelog.rst | 9 | ||||
-rw-r--r-- | tests/configs/basic.config | 6 | ||||
-rw-r--r-- | tests/configs/canon-always.config | 8 | ||||
-rw-r--r-- | tests/configs/canon-ipv4.config | 9 | ||||
-rw-r--r-- | tests/configs/canon-local-always.config | 9 | ||||
-rw-r--r-- | tests/configs/canon-local.config | 9 | ||||
-rw-r--r-- | tests/configs/canon.config | 11 | ||||
-rw-r--r-- | tests/configs/deep-canon-maxdots.config | 14 | ||||
-rw-r--r-- | tests/configs/deep-canon.config | 13 | ||||
-rw-r--r-- | tests/configs/empty-canon.config | 9 | ||||
-rw-r--r-- | tests/configs/fallback-no.config | 9 | ||||
-rw-r--r-- | tests/configs/fallback-yes.config | 8 | ||||
-rw-r--r-- | tests/configs/multi-canon-domains.config | 8 | ||||
-rw-r--r-- | tests/configs/no-canon.config | 8 | ||||
-rw-r--r-- | tests/configs/robey.config (renamed from tests/robey.config) | 0 | ||||
-rw-r--r-- | tests/configs/zero-maxdots.config | 11 | ||||
-rw-r--r-- | tests/test_config.py | 157 | ||||
-rw-r--r-- | tests/test_util.py | 73 | ||||
-rw-r--r-- | tests/util.py | 9 |
23 files changed, 484 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..6430f1e0 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,90 @@ 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") + 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() + hostname = self.canonicalize(hostname, options, domains) + options["hostname"] = hostname + options = self._lookup(hostname, options) + 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( + x for x in value if x not in options[key] + ) + 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 +359,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 +419,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..7cc39f34 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -2,6 +2,15 @@ Changelog ========= +- :bug:`- major` Perform deduplication of ``IdentityFile`` contents during + ``ssh_config`` parsing; previously, if your config would result in the same + value being encountered more than once, ``IdentityFile`` would contain that + many copies of the same string. +- :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..7a7ce6c6 --- /dev/null +++ b/tests/configs/canon.config @@ -0,0 +1,11 @@ +CanonicalizeHostname yes +CanonicalDomains paramiko.org + +IdentityFile base.key + +Host www.paramiko.org + User rando + IdentityFile canonicalized.key + + +# 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..f2c89bb5 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, 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,141 @@ 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_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): + result = load_config("canon").lookup("www") + assert result["identityfile"] == ["base.key", "canonicalized.key"] + + +@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( |