diff options
Diffstat (limited to 'tests')
57 files changed, 1606 insertions, 508 deletions
diff --git a/tests/configs/basic b/tests/configs/basic new file mode 100644 index 00000000..93fe3beb --- /dev/null +++ b/tests/configs/basic @@ -0,0 +1,4 @@ +CanonicalDomains paramiko.org + +Host www.paramiko.org + User rando diff --git a/tests/configs/canon b/tests/configs/canon new file mode 100644 index 00000000..7b979408 --- /dev/null +++ b/tests/configs/canon @@ -0,0 +1,8 @@ +CanonicalizeHostname yes +CanonicalDomains paramiko.org + +IdentityFile base.key + +Host www.paramiko.org + User rando + IdentityFile canonicalized.key diff --git a/tests/configs/canon-always b/tests/configs/canon-always new file mode 100644 index 00000000..f3f56b70 --- /dev/null +++ b/tests/configs/canon-always @@ -0,0 +1,5 @@ +CanonicalDomains paramiko.org +CanonicalizeHostname always + +Host www.paramiko.org + User rando diff --git a/tests/configs/canon-ipv4 b/tests/configs/canon-ipv4 new file mode 100644 index 00000000..92c3875f --- /dev/null +++ b/tests/configs/canon-ipv4 @@ -0,0 +1,6 @@ +CanonicalDomains paramiko.org +CanonicalizeHostname yes +AddressFamily inet + +Host www.paramiko.org + User rando diff --git a/tests/configs/canon-local b/tests/configs/canon-local new file mode 100644 index 00000000..dde9f77b --- /dev/null +++ b/tests/configs/canon-local @@ -0,0 +1,6 @@ +Host www.paramiko.org + User rando + +Host www + CanonicalDomains paramiko.org + CanonicalizeHostname yes diff --git a/tests/configs/canon-local-always b/tests/configs/canon-local-always new file mode 100644 index 00000000..0ad0535a --- /dev/null +++ b/tests/configs/canon-local-always @@ -0,0 +1,6 @@ +Host www.paramiko.org + User rando + +Host www + CanonicalDomains paramiko.org + CanonicalizeHostname always diff --git a/tests/configs/deep-canon b/tests/configs/deep-canon new file mode 100644 index 00000000..483823d5 --- /dev/null +++ b/tests/configs/deep-canon @@ -0,0 +1,11 @@ +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 diff --git a/tests/configs/deep-canon-maxdots b/tests/configs/deep-canon-maxdots new file mode 100644 index 00000000..7785f660 --- /dev/null +++ b/tests/configs/deep-canon-maxdots @@ -0,0 +1,12 @@ +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 diff --git a/tests/configs/empty-canon b/tests/configs/empty-canon new file mode 100644 index 00000000..19743ad6 --- /dev/null +++ b/tests/configs/empty-canon @@ -0,0 +1,6 @@ +CanonicalizeHostname yes +CanonicalDomains +AddressFamily inet + +Host www.paramiko.org + User rando diff --git a/tests/configs/fallback-no b/tests/configs/fallback-no new file mode 100644 index 00000000..ec8d13ee --- /dev/null +++ b/tests/configs/fallback-no @@ -0,0 +1,6 @@ +CanonicalizeHostname yes +CanonicalDomains paramiko.org +CanonicalizeFallbackLocal no + +Host www.paramiko.org + User rando diff --git a/tests/configs/fallback-yes b/tests/configs/fallback-yes new file mode 100644 index 00000000..bc4f4eee --- /dev/null +++ b/tests/configs/fallback-yes @@ -0,0 +1,6 @@ +CanonicalizeHostname yes +CanonicalDomains paramiko.org +CanonicalizeFallbackLocal yes + +Host www.paramiko.org + User rando diff --git a/tests/configs/hostname-exec-tokenized b/tests/configs/hostname-exec-tokenized new file mode 100644 index 00000000..1cae2c03 --- /dev/null +++ b/tests/configs/hostname-exec-tokenized @@ -0,0 +1,2 @@ +Match exec "ping %h" + HostName pingable.%h diff --git a/tests/configs/hostname-tokenized b/tests/configs/hostname-tokenized new file mode 100644 index 00000000..1905c0cc --- /dev/null +++ b/tests/configs/hostname-tokenized @@ -0,0 +1 @@ +HostName prefix.%h diff --git a/tests/configs/invalid b/tests/configs/invalid new file mode 100644 index 00000000..81332fe8 --- /dev/null +++ b/tests/configs/invalid @@ -0,0 +1 @@ +lolwut diff --git a/tests/configs/match-all b/tests/configs/match-all new file mode 100644 index 00000000..7673e0a0 --- /dev/null +++ b/tests/configs/match-all @@ -0,0 +1,2 @@ +Match all + User awesome diff --git a/tests/configs/match-all-after-canonical b/tests/configs/match-all-after-canonical new file mode 100644 index 00000000..531112cb --- /dev/null +++ b/tests/configs/match-all-after-canonical @@ -0,0 +1,5 @@ +CanonicalizeHostname yes +CanonicalDomains paramiko.org + +Match canonical all + User awesome diff --git a/tests/configs/match-all-and-more b/tests/configs/match-all-and-more new file mode 100644 index 00000000..bb50696e --- /dev/null +++ b/tests/configs/match-all-and-more @@ -0,0 +1,2 @@ +Match all exec "lol nope" + HostName whatever diff --git a/tests/configs/match-all-and-more-before b/tests/configs/match-all-and-more-before new file mode 100644 index 00000000..4d5b2e34 --- /dev/null +++ b/tests/configs/match-all-and-more-before @@ -0,0 +1,2 @@ +Match exec "lol nope" all + HostName whatever diff --git a/tests/configs/match-all-before-canonical b/tests/configs/match-all-before-canonical new file mode 100644 index 00000000..35e3b0e2 --- /dev/null +++ b/tests/configs/match-all-before-canonical @@ -0,0 +1,5 @@ +CanonicalizeHostname yes +CanonicalDomains paramiko.org + +Match all canonical + User oops diff --git a/tests/configs/match-canonical-no b/tests/configs/match-canonical-no new file mode 100644 index 00000000..e528dc64 --- /dev/null +++ b/tests/configs/match-canonical-no @@ -0,0 +1,7 @@ +CanonicalizeHostname no + +Match canonical all + User awesome + +Match !canonical host specific + User overload diff --git a/tests/configs/match-canonical-yes b/tests/configs/match-canonical-yes new file mode 100644 index 00000000..d6c20928 --- /dev/null +++ b/tests/configs/match-canonical-yes @@ -0,0 +1,5 @@ +CanonicalizeHostname yes +CanonicalDomains paramiko.org + +Match !canonical host www* + User hidden diff --git a/tests/configs/match-complex b/tests/configs/match-complex new file mode 100644 index 00000000..63634039 --- /dev/null +++ b/tests/configs/match-complex @@ -0,0 +1,17 @@ +HostName bogus + +Match originalhost target host bogus + User rand + +Match originalhost remote localuser rando + User calrissian + +# Just to set user for subsequent match +Match originalhost www + User calrissian + +Match !canonical originalhost www host bogus localuser rando user calrissian + Port 7777 + +Match !canonical !originalhost www host bogus localuser rando !user calrissian + Port 1234 diff --git a/tests/configs/match-exec b/tests/configs/match-exec new file mode 100644 index 00000000..763346ea --- /dev/null +++ b/tests/configs/match-exec @@ -0,0 +1,16 @@ +Match exec "quoted" + User benjamin + +Match exec unquoted + User rando + +Match exec "quoted spaced" + User neil + +# Just to prepopulate values for tokenizing subsequent exec +Host target + User intermediate + HostName configured + +Match exec "%d %h %L %l %n %p %r %u" + Port 1337 diff --git a/tests/configs/match-exec-canonical b/tests/configs/match-exec-canonical new file mode 100644 index 00000000..794ee9d5 --- /dev/null +++ b/tests/configs/match-exec-canonical @@ -0,0 +1,10 @@ +CanonicalDomains paramiko.org +CanonicalizeHostname always + +# This will match in the first, uncanonicalized pass +Match !canonical exec uncanonicalized + User defenseless + +# And this will match the second time +Match canonical exec canonicalized + Port 8007 diff --git a/tests/configs/match-exec-negation b/tests/configs/match-exec-negation new file mode 100644 index 00000000..937c910e --- /dev/null +++ b/tests/configs/match-exec-negation @@ -0,0 +1,5 @@ +Match !exec "this succeeds" + User nope + +Match !exec "this fails" + User yup diff --git a/tests/configs/match-exec-no-arg b/tests/configs/match-exec-no-arg new file mode 100644 index 00000000..20c16d16 --- /dev/null +++ b/tests/configs/match-exec-no-arg @@ -0,0 +1,2 @@ +Match exec + User uh-oh diff --git a/tests/configs/match-host b/tests/configs/match-host new file mode 100644 index 00000000..86cbff5d --- /dev/null +++ b/tests/configs/match-host @@ -0,0 +1,2 @@ +Match host target + User rand diff --git a/tests/configs/match-host-canonicalized b/tests/configs/match-host-canonicalized new file mode 100644 index 00000000..52dadeae --- /dev/null +++ b/tests/configs/match-host-canonicalized @@ -0,0 +1,8 @@ +CanonicalizeHostname yes +CanonicalDomains paramiko.org + +Match host www.paramiko.org + User rand + +Match canonical host docs.paramiko.org + User eric diff --git a/tests/configs/match-host-from-match b/tests/configs/match-host-from-match new file mode 100644 index 00000000..172ee116 --- /dev/null +++ b/tests/configs/match-host-from-match @@ -0,0 +1,5 @@ +Match host original-host + HostName substituted-host + +Match host substituted-host + User inner diff --git a/tests/configs/match-host-glob b/tests/configs/match-host-glob new file mode 100644 index 00000000..3d53cf48 --- /dev/null +++ b/tests/configs/match-host-glob @@ -0,0 +1,2 @@ +Match host *ever + User matrim diff --git a/tests/configs/match-host-glob-list b/tests/configs/match-host-glob-list new file mode 100644 index 00000000..3617d136 --- /dev/null +++ b/tests/configs/match-host-glob-list @@ -0,0 +1,8 @@ +Match host *ever + User matrim + +Match host somehost,someotherhost + User thom + +Match host goo*,!goof + User perrin diff --git a/tests/configs/match-host-name b/tests/configs/match-host-name new file mode 100644 index 00000000..783d939e --- /dev/null +++ b/tests/configs/match-host-name @@ -0,0 +1,4 @@ +HostName default-host + +Match host default-host + User silly diff --git a/tests/configs/match-host-negated b/tests/configs/match-host-negated new file mode 100644 index 00000000..7c5d3f3e --- /dev/null +++ b/tests/configs/match-host-negated @@ -0,0 +1,2 @@ +Match !host www + User jeff diff --git a/tests/configs/match-host-no-arg b/tests/configs/match-host-no-arg new file mode 100644 index 00000000..191cebb5 --- /dev/null +++ b/tests/configs/match-host-no-arg @@ -0,0 +1,2 @@ +Match host + User oops diff --git a/tests/configs/match-localuser b/tests/configs/match-localuser new file mode 100644 index 00000000..fe4a276c --- /dev/null +++ b/tests/configs/match-localuser @@ -0,0 +1,14 @@ +Match localuser gandalf + HostName gondor + +Match localuser b* + HostName shire + +Match localuser aragorn,frodo + HostName moria + +Match localuser gimli,!legolas + Port 7373 + +Match !localuser sauron + HostName mordor diff --git a/tests/configs/match-localuser-no-arg b/tests/configs/match-localuser-no-arg new file mode 100644 index 00000000..6623553a --- /dev/null +++ b/tests/configs/match-localuser-no-arg @@ -0,0 +1,2 @@ +Match localuser + User oops diff --git a/tests/configs/match-orighost b/tests/configs/match-orighost new file mode 100644 index 00000000..10541993 --- /dev/null +++ b/tests/configs/match-orighost @@ -0,0 +1,16 @@ +HostName bogus + +Match originalhost target + User tuon + +Match originalhost what* + User matrim + +Match originalhost comma,sep* + User chameleon + +Match originalhost yep,!nope + User skipped + +Match !originalhost www !originalhost nope + User thom diff --git a/tests/configs/match-orighost-canonical b/tests/configs/match-orighost-canonical new file mode 100644 index 00000000..737345e8 --- /dev/null +++ b/tests/configs/match-orighost-canonical @@ -0,0 +1,5 @@ +CanonicalizeHostname yes +CanonicalDomains paramiko.org + +Match originalhost www + User tuon diff --git a/tests/configs/match-orighost-no-arg b/tests/configs/match-orighost-no-arg new file mode 100644 index 00000000..427382ba --- /dev/null +++ b/tests/configs/match-orighost-no-arg @@ -0,0 +1,2 @@ +Match originalhost + User oops diff --git a/tests/configs/match-user b/tests/configs/match-user new file mode 100644 index 00000000..14d6ac12 --- /dev/null +++ b/tests/configs/match-user @@ -0,0 +1,14 @@ +Match user gandalf + HostName gondor + +Match user b* + HostName shire + +Match user aragorn,frodo + HostName moria + +Match user gimli,!legolas + Port 7373 + +Match !user sauron + HostName mordor diff --git a/tests/configs/match-user-explicit b/tests/configs/match-user-explicit new file mode 100644 index 00000000..9a2b1d82 --- /dev/null +++ b/tests/configs/match-user-explicit @@ -0,0 +1,4 @@ +User explicit + +Match user explicit + HostName dumb diff --git a/tests/configs/match-user-no-arg b/tests/configs/match-user-no-arg new file mode 100644 index 00000000..65a11ab4 --- /dev/null +++ b/tests/configs/match-user-no-arg @@ -0,0 +1,2 @@ +Match user + User oops diff --git a/tests/configs/multi-canon-domains b/tests/configs/multi-canon-domains new file mode 100644 index 00000000..5674b442 --- /dev/null +++ b/tests/configs/multi-canon-domains @@ -0,0 +1,5 @@ +CanonicalizeHostname yes +CanonicalDomains not-a-real-tld paramiko.org + +Host www.paramiko.org + User rando diff --git a/tests/configs/no-canon b/tests/configs/no-canon new file mode 100644 index 00000000..033f8c53 --- /dev/null +++ b/tests/configs/no-canon @@ -0,0 +1,5 @@ +CanonicalizeHostname no +CanonicalDomains paramiko.org + +Host www.paramiko.org + User rando diff --git a/tests/configs/robey b/tests/configs/robey new file mode 100644 index 00000000..b2026224 --- /dev/null +++ b/tests/configs/robey @@ -0,0 +1,17 @@ +# A timeless classic? +# NOTE: some lines in here have 'extra' whitespace (incl trailing, and mixed +# tabs/spaces!) on purpose. + +Host * + User robey + IdentityFile =~/.ssh/id_rsa + +# comment +Host *.example.com + User bjork +Port=3333 +Host * + Crazy something dumb +Host spoo.example.com +Crazy something else + diff --git a/tests/configs/zero-maxdots b/tests/configs/zero-maxdots new file mode 100644 index 00000000..dc00054c --- /dev/null +++ b/tests/configs/zero-maxdots @@ -0,0 +1,9 @@ +CanonicalizeHostname yes +CanonicalDomains paramiko.org +CanonicalizeMaxDots 0 + +Host www.paramiko.org + User rando + +Host sub.www.paramiko.org + User deep diff --git a/tests/test_client.py b/tests/test_client.py index ad5c36ad..60ad310c 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -407,15 +407,15 @@ class SSHClientTest(ClientTest): self.tc = SSHClient() self.tc.set_missing_host_key_policy(paramiko.AutoAddPolicy()) - self.assertEqual(0, len(self.tc.get_host_keys())) + assert len(self.tc.get_host_keys()) == 0 self.tc.connect(**dict(self.connect_kwargs, password="pygmalion")) self.event.wait(1.0) - self.assertTrue(self.event.is_set()) - self.assertTrue(self.ts.is_active()) + assert self.event.is_set() + assert self.ts.is_active() p = weakref.ref(self.tc._transport.packetizer) - self.assertTrue(p() is not None) + assert p() is not None self.tc.close() del self.tc @@ -425,7 +425,7 @@ class SSHClientTest(ClientTest): gc.collect() gc.collect() - self.assertTrue(p() is None) + assert p() is None def test_client_can_be_used_as_context_manager(self): """ diff --git a/tests/test_config.py b/tests/test_config.py index cbd3f623..5e9aa059 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -1,74 +1,992 @@ # This file is part of Paramiko and subject to the license in /LICENSE in this # repository -import pytest +from os.path import expanduser +from socket import gaierror -from paramiko import config -from paramiko.util import parse_ssh_config -from paramiko.py3compat import StringIO +from paramiko.py3compat import string_types +from invoke import Result +from mock import patch +from pytest import raises, mark, fixture -def test_SSHConfigDict_construct_empty(): - assert not config.SSHConfigDict() +from paramiko import ( + SSHConfig, + SSHConfigDict, + CouldNotCanonicalize, + ConfigParseError, +) +from .util import _config -def test_SSHConfigDict_construct_from_list(): - assert config.SSHConfigDict([(1, 2)])[1] == 2 +@fixture +def socket(): + """ + Patch all of socket.* in our config module to prevent eg real DNS lookups. + + Also forces getaddrinfo (used in our addressfamily lookup stuff) to always + fail by default to mimic usual lack of AddressFamily related crap. -def test_SSHConfigDict_construct_from_dict(): - assert config.SSHConfigDict({1: 2})[1] == 2 + Callers who want to mock DNS lookups can then safely assume gethostbyname() + will be in use. + """ + 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 + # Patch out getfqdn to return some real string for when it gets called; + # some code (eg tokenization) gets mad w/ MagicMocks + mocket.getfqdn.return_value = "some.fake.fqdn" + yield mocket -@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 +def load_config(name): + return SSHConfig.from_path(_config(name)) -@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 +class TestSSHConfig(object): + def setup(self): + self.config = load_config("robey") + def test_init(self): + # No args! + with raises(TypeError): + SSHConfig("uh oh!") + # No args. + assert not SSHConfig()._config -@pytest.mark.parametrize("int_val", ("42", 42)) -def test_SSHConfigDict_as_int(int_val): - assert config.SSHConfigDict({"key": int_val}).as_int("key") == 42 + def test_from_text(self): + config = SSHConfig.from_text("User foo") + assert config.lookup("foo.example.com")["user"] == "foo" + def test_from_file(self): + with open(_config("robey")) as flo: + config = SSHConfig.from_file(flo) + assert config.lookup("whatever")["user"] == "robey" -@pytest.mark.parametrize("non_int", ("not an int", None, object())) -def test_SSHConfigDict_as_int_failures(non_int): - conf = config.SSHConfigDict({"key": non_int}) + def test_from_path(self): + # NOTE: DO NOT replace with use of load_config() :D + config = SSHConfig.from_path(_config("robey")) + assert config.lookup("meh.example.com")["port"] == "3333" - try: - int(non_int) - except Exception as e: - exception_type = type(e) + def test_parse_config(self): + expected = [ + {"host": ["*"], "config": {}}, + { + "host": ["*"], + "config": {"identityfile": ["~/.ssh/id_rsa"], "user": "robey"}, + }, + { + "host": ["*.example.com"], + "config": {"user": "bjork", "port": "3333"}, + }, + {"host": ["*"], "config": {"crazy": "something dumb"}}, + { + "host": ["spoo.example.com"], + "config": {"crazy": "something else"}, + }, + ] + assert self.config._config == expected - with pytest.raises(exception_type): - conf.as_int("key") + @mark.parametrize( + "host,values", + ( + ( + "irc.danger.com", + { + "crazy": "something dumb", + "hostname": "irc.danger.com", + "user": "robey", + }, + ), + ( + "irc.example.com", + { + "crazy": "something dumb", + "hostname": "irc.example.com", + "user": "robey", + "port": "3333", + }, + ), + ( + "spoo.example.com", + { + "crazy": "something dumb", + "hostname": "spoo.example.com", + "user": "robey", + "port": "3333", + }, + ), + ), + ) + def test_host_config(self, host, values): + expected = dict( + values, hostname=host, identityfile=[expanduser("~/.ssh/id_rsa")] + ) + assert self.config.lookup(host) == expected + def test_fabric_issue_33(self): + config = SSHConfig.from_text( + """ +Host www13.* + Port 22 -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 +""" + ) + host = "www13.example.com" + expected = {"hostname": host, "port": "22"} + assert config.lookup(host) == expected + + def test_proxycommand_config_equals_parsing(self): + """ + ProxyCommand should not split on equals signs within the value. + """ + config = SSHConfig.from_text( + """ +Host space-delimited + ProxyCommand foo bar=biz baz + +Host equals-delimited + ProxyCommand=foo bar=biz baz +""" + ) + for host in ("space-delimited", "equals-delimited"): + value = config.lookup(host)["proxycommand"] + assert value == "foo bar=biz baz" + + def test_proxycommand_interpolation(self): + """ + ProxyCommand should perform interpolation on the value + """ + config = SSHConfig.from_text( + """ +Host specific + Port 37 + ProxyCommand host %h port %p lol + +Host portonly + Port 155 + +Host * + Port 25 + ProxyCommand host %h port %p +""" + ) + for host, val in ( + ("foo.com", "host foo.com port 25"), + ("specific", "host specific port 37 lol"), + ("portonly", "host portonly port 155"), + ): + assert config.lookup(host)["proxycommand"] == val + + def test_proxycommand_tilde_expansion(self): + """ + Tilde (~) should be expanded inside ProxyCommand + """ + config = SSHConfig.from_text( + """ +Host test + ProxyCommand ssh -F ~/.ssh/test_config bastion nc %h %p +""" + ) + expected = "ssh -F {}/.ssh/test_config bastion nc test 22".format( + expanduser("~") + ) + got = config.lookup("test")["proxycommand"] + assert got == expected + + @patch("paramiko.config.getpass") + def test_controlpath_token_expansion(self, getpass): + getpass.getuser.return_value = "gandalf" + config = SSHConfig.from_text( + """ +Host explicit_user + User root + ControlPath user %u remoteuser %r + +Host explicit_host + HostName ohai + ControlPath remoteuser %r host %h orighost %n + """ + ) + result = config.lookup("explicit_user")["controlpath"] + # Remote user is User val, local user is User val + assert result == "user gandalf remoteuser root" + result = config.lookup("explicit_host")["controlpath"] + # Remote user falls back to local user; host and orighost may differ + assert result == "remoteuser gandalf host ohai orighost explicit_host" + + def test_negation(self): + config = SSHConfig.from_text( + """ +Host www13.* !*.example.com + Port 22 + +Host *.example.com !www13.* + Port 2222 + +Host www13.* + Port 8080 + +Host * + Port 3333 +""" + ) + host = "www13.example.com" + expected = {"hostname": host, "port": "8080"} + assert config.lookup(host) == expected + + def test_proxycommand(self): + config = SSHConfig.from_text( + """ +Host proxy-with-equal-divisor-and-space +ProxyCommand = foo=bar + +Host proxy-with-equal-divisor-and-no-space +ProxyCommand=foo=bar + +Host proxy-without-equal-divisor +ProxyCommand foo=bar:%h-%p +""" + ) + for host, values in { + "proxy-with-equal-divisor-and-space": { + "hostname": "proxy-with-equal-divisor-and-space", + "proxycommand": "foo=bar", + }, + "proxy-with-equal-divisor-and-no-space": { + "hostname": "proxy-with-equal-divisor-and-no-space", + "proxycommand": "foo=bar", + }, + "proxy-without-equal-divisor": { + "hostname": "proxy-without-equal-divisor", + "proxycommand": "foo=bar:proxy-without-equal-divisor-22", + }, + }.items(): + + assert config.lookup(host) == values + + def test_identityfile(self): + config = SSHConfig.from_text( + """ + +IdentityFile id_dsa0 + +Host * +IdentityFile id_dsa1 + +Host dsa2 +IdentityFile id_dsa2 + +Host dsa2* +IdentityFile id_dsa22 +""" + ) + for host, values in { + "foo": {"hostname": "foo", "identityfile": ["id_dsa0", "id_dsa1"]}, + "dsa2": { + "hostname": "dsa2", + "identityfile": ["id_dsa0", "id_dsa1", "id_dsa2", "id_dsa22"], + }, + "dsa22": { + "hostname": "dsa22", + "identityfile": ["id_dsa0", "id_dsa1", "id_dsa22"], + }, + }.items(): + + assert config.lookup(host) == values + + def test_config_addressfamily_and_lazy_fqdn(self): + """ + Ensure the code path honoring non-'all' AddressFamily doesn't asplode + """ + config = SSHConfig.from_text( + """ +AddressFamily inet +IdentityFile something_%l_using_fqdn +""" + ) + assert config.lookup( + "meh" + ) # will die during lookup() if bug regresses + + def test_config_dos_crlf_succeeds(self): + config = SSHConfig.from_text( + """ +Host abcqwerty\r\nHostName 127.0.0.1\r\n +""" + ) + assert config.lookup("abcqwerty")["hostname"] == "127.0.0.1" + + def test_get_hostnames(self): + expected = {"*", "*.example.com", "spoo.example.com"} + assert self.config.get_hostnames() == expected + + def test_quoted_host_names(self): + config = SSHConfig.from_text( + """ +Host "param pam" param "pam" + Port 1111 + +Host "param2" + Port 2222 + +Host param3 parara + Port 3333 +Host param4 "p a r" "p" "par" para + Port 4444 +""" + ) + res = { + "param pam": {"hostname": "param pam", "port": "1111"}, + "param": {"hostname": "param", "port": "1111"}, + "pam": {"hostname": "pam", "port": "1111"}, + "param2": {"hostname": "param2", "port": "2222"}, + "param3": {"hostname": "param3", "port": "3333"}, + "parara": {"hostname": "parara", "port": "3333"}, + "param4": {"hostname": "param4", "port": "4444"}, + "p a r": {"hostname": "p a r", "port": "4444"}, + "p": {"hostname": "p", "port": "4444"}, + "par": {"hostname": "par", "port": "4444"}, + "para": {"hostname": "para", "port": "4444"}, + } + for host, values in res.items(): + assert config.lookup(host) == values -def test_SSHConfig_wildcard_host_dicts_are_SSHConfigDict_instances(): - test_config_file = """\ + def test_quoted_params_in_config(self): + config = SSHConfig.from_text( + """ +Host "param pam" param "pam" + IdentityFile id_rsa + +Host "param2" + IdentityFile "test rsa key" + +Host param3 parara + IdentityFile id_rsa + IdentityFile "test rsa key" +""" + ) + res = { + "param pam": {"hostname": "param pam", "identityfile": ["id_rsa"]}, + "param": {"hostname": "param", "identityfile": ["id_rsa"]}, + "pam": {"hostname": "pam", "identityfile": ["id_rsa"]}, + "param2": {"hostname": "param2", "identityfile": ["test rsa key"]}, + "param3": { + "hostname": "param3", + "identityfile": ["id_rsa", "test rsa key"], + }, + "parara": { + "hostname": "parara", + "identityfile": ["id_rsa", "test rsa key"], + }, + } + for host, values in res.items(): + assert config.lookup(host) == values + + def test_quoted_host_in_config(self): + conf = SSHConfig() + correct_data = { + "param": ["param"], + '"param"': ["param"], + "param pam": ["param", "pam"], + '"param" "pam"': ["param", "pam"], + '"param" pam': ["param", "pam"], + 'param "pam"': ["param", "pam"], + 'param "pam" p': ["param", "pam", "p"], + '"param" pam "p"': ["param", "pam", "p"], + '"pa ram"': ["pa ram"], + '"pa ram" pam': ["pa ram", "pam"], + 'param "p a m"': ["param", "p a m"], + } + incorrect_data = ['param"', '"param', 'param "pam', 'param "pam" "p a'] + for host, values in correct_data.items(): + assert conf._get_hosts(host) == values + for host in incorrect_data: + with raises(ConfigParseError): + conf._get_hosts(host) + + def test_invalid_line_format_excepts(self): + with raises(ConfigParseError): + load_config("invalid") + + def test_proxycommand_none_issue_418(self): + config = SSHConfig.from_text( + """ +Host proxycommand-standard-none + ProxyCommand None + +Host proxycommand-with-equals-none + ProxyCommand=None +""" + ) + for host, values in { + "proxycommand-standard-none": { + "hostname": "proxycommand-standard-none" + }, + "proxycommand-with-equals-none": { + "hostname": "proxycommand-with-equals-none" + }, + }.items(): + + assert config.lookup(host) == values + + def test_proxycommand_none_masking(self): + # Re: https://github.com/paramiko/paramiko/issues/670 + config = SSHConfig.from_text( + """ +Host specific-host + ProxyCommand none + +Host other-host + ProxyCommand other-proxy + +Host * + ProxyCommand default-proxy +""" + ) + # When bug is present, the full stripping-out of specific-host's + # ProxyCommand means it actually appears to pick up the default + # ProxyCommand value instead, due to cascading. It should (for + # backwards compatibility reasons in 1.x/2.x) appear completely blank, + # as if the host had no ProxyCommand whatsoever. + # Threw another unrelated host in there just for sanity reasons. + assert "proxycommand" not in config.lookup("specific-host") + assert config.lookup("other-host")["proxycommand"] == "other-proxy" + cmd = config.lookup("some-random-host")["proxycommand"] + assert cmd == "default-proxy" + + def test_hostname_tokenization(self): + result = load_config("hostname-tokenized").lookup("whatever") + assert result["hostname"] == "prefix.whatever" + + +class TestSSHConfigDict(object): + def test_SSHConfigDict_construct_empty(self): + assert not SSHConfigDict() + + def test_SSHConfigDict_construct_from_list(self): + assert SSHConfigDict([(1, 2)])[1] == 2 + + def test_SSHConfigDict_construct_from_dict(self): + assert SSHConfigDict({1: 2})[1] == 2 + + @mark.parametrize("true_ish", ("yes", "YES", "Yes", True)) + def test_SSHConfigDict_as_bool_true_ish(self, true_ish): + assert SSHConfigDict({"key": true_ish}).as_bool("key") is True + + @mark.parametrize("false_ish", ("no", "NO", "No", False)) + def test_SSHConfigDict_as_bool(self, false_ish): + assert SSHConfigDict({"key": false_ish}).as_bool("key") is False + + @mark.parametrize("int_val", ("42", 42)) + def test_SSHConfigDict_as_int(self, int_val): + assert SSHConfigDict({"key": int_val}).as_int("key") == 42 + + @mark.parametrize("non_int", ("not an int", None, object())) + def test_SSHConfigDict_as_int_failures(self, non_int): + conf = SSHConfigDict({"key": non_int}) + + try: + int(non_int) + except Exception as e: + exception_type = type(e) + + with raises(exception_type): + conf.as_int("key") + + def test_SSHConfig_host_dicts_are_SSHConfigDict_instances(self): + config = SSHConfig.from_text( + """ +Host *.example.com + Port 2222 + +Host * + Port 3333 +""" + ) + assert config.lookup("foo.example.com").as_int("port") == 2222 + + def test_SSHConfig_wildcard_host_dicts_are_SSHConfigDict_instances(self): + config = SSHConfig.from_text( + """ Host *.example.com Port 2222 Host * Port 3333 +""" + ) + assert config.lookup("anything-else").as_int("port") == 3333 + + +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 + + +class TestMatchAll(object): + def test_always_matches(self): + result = load_config("match-all").lookup("general") + assert result["user"] == "awesome" + + def test_may_not_mix_with_non_canonical_keywords(self): + for config in ("match-all-and-more", "match-all-and-more-before"): + with raises(ConfigParseError): + load_config(config).lookup("whatever") + + def test_may_come_after_canonical(self, socket): + result = load_config("match-all-after-canonical").lookup("www") + assert result["user"] == "awesome" + + def test_may_not_come_before_canonical(self, socket): + with raises(ConfigParseError): + load_config("match-all-before-canonical") + + def test_after_canonical_not_loaded_when_non_canonicalized(self, socket): + result = load_config("match-canonical-no").lookup("a-host") + assert "user" not in result + + +def _expect(success_on): + """ + Returns a side_effect-friendly Invoke success result for given command(s). + + Ensures that any other commands fail; this is useful for testing 'Match + exec' because it means all other such clauses under test act like no-ops. + + :param success_on: + Single string or list of strings, noting commands that should appear to + succeed. """ - f = StringIO(test_config_file) - config = parse_ssh_config(f) - assert config.lookup("anything-else").as_int("port") == 3333 + if isinstance(success_on, string_types): + success_on = [success_on] + + def inner(command, *args, **kwargs): + # Sanity checking - we always expect that invoke.run is called with + # these. + assert kwargs.get("hide", None) == "stdout" + assert kwargs.get("warn", None) is True + # Fake exit + exit = 0 if command in success_on else 1 + return Result(exited=exit) + + return inner + + +class TestMatchExec(object): + @patch("paramiko.config.invoke", new=None) + @patch("paramiko.config.invoke_import_error", new=ImportError("meh")) + def test_raises_invoke_ImportErrors_at_runtime(self): + # Not an ideal test, but I don't know of a non-bad way to fake out + # module-time ImportErrors. So we mock the symptoms. Meh! + with raises(ImportError) as info: + load_config("match-exec").lookup("oh-noes") + assert str(info.value) == "meh" + + @patch("paramiko.config.invoke.run") + @mark.parametrize( + "cmd,user", + [ + ("unquoted", "rando"), + ("quoted", "benjamin"), + ("quoted spaced", "neil"), + ], + ) + def test_accepts_single_possibly_quoted_argument(self, run, cmd, user): + run.side_effect = _expect(cmd) + result = load_config("match-exec").lookup("whatever") + assert result["user"] == user + + @patch("paramiko.config.invoke.run") + def test_does_not_match_nonzero_exit_codes(self, run): + # Nothing will succeed -> no User ever gets loaded + run.return_value = Result(exited=1) + result = load_config("match-exec").lookup("whatever") + assert "user" not in result + + @patch("paramiko.config.getpass") + @patch("paramiko.config.invoke.run") + def test_tokenizes_argument(self, run, getpass, socket): + socket.gethostname.return_value = "local.fqdn" + getpass.getuser.return_value = "gandalf" + # Actual exec value is "%d %h %L %l %n %p %r %u" + parts = ( + expanduser("~"), + "configured", + "local", + "some.fake.fqdn", + "target", + "22", + "intermediate", + "gandalf", + ) + run.side_effect = _expect(" ".join(parts)) + result = load_config("match-exec").lookup("target") + assert result["port"] == "1337" + + @patch("paramiko.config.invoke.run") + def test_works_with_canonical(self, run, socket): + # Ensure both stanzas' exec components appear to match + run.side_effect = _expect(["uncanonicalized", "canonicalized"]) + result = load_config("match-exec-canonical").lookup("who-cares") + # Prove both config values got loaded up, across the two passes + assert result["user"] == "defenseless" + assert result["port"] == "8007" + + @patch("paramiko.config.invoke.run") + def test_may_be_negated(self, run): + run.side_effect = _expect("this succeeds") + result = load_config("match-exec-negation").lookup("so-confusing") + # If negation did not work, the first of the two Match exec directives + # would have set User to 'nope' (and/or the second would have NOT set + # User to 'yup') + assert result["user"] == "yup" + + def test_requires_an_argument(self): + with raises(ConfigParseError): + load_config("match-exec-no-arg") + + @patch("paramiko.config.invoke.run") + def test_works_with_tokenized_hostname(self, run): + run.side_effect = _expect("ping target") + result = load_config("hostname-exec-tokenized").lookup("target") + assert result["hostname"] == "pingable.target" + + +class TestMatchHost(object): + def test_matches_target_name_when_no_hostname(self): + result = load_config("match-host").lookup("target") + assert result["user"] == "rand" + + def test_matches_hostname_from_global_setting(self): + # Also works for ones set in regular Host stanzas + result = load_config("match-host-name").lookup("anything") + assert result["user"] == "silly" + + def test_matches_hostname_from_earlier_match(self): + # Corner case: one Match matches original host, sets HostName, + # subsequent Match matches the latter. + result = load_config("match-host-from-match").lookup("original-host") + assert result["user"] == "inner" + + def test_may_be_globbed(self): + result = load_config("match-host-glob-list").lookup("whatever") + assert result["user"] == "matrim" + + def test_may_be_comma_separated_list(self): + for target in ("somehost", "someotherhost"): + result = load_config("match-host-glob-list").lookup(target) + assert result["user"] == "thom" + + def test_comma_separated_list_may_have_internal_negation(self): + conf = load_config("match-host-glob-list") + assert conf.lookup("good")["user"] == "perrin" + assert "user" not in conf.lookup("goof") + + def test_matches_canonicalized_name(self, socket): + # Without 'canonical' explicitly declared, mind. + result = load_config("match-host-canonicalized").lookup("www") + assert result["user"] == "rand" + + def test_works_with_canonical_keyword(self, socket): + # NOTE: distinct from 'happens to be canonicalized' above + result = load_config("match-host-canonicalized").lookup("docs") + assert result["user"] == "eric" + + def test_may_be_negated(self): + conf = load_config("match-host-negated") + assert conf.lookup("docs")["user"] == "jeff" + assert "user" not in conf.lookup("www") + + def test_requires_an_argument(self): + with raises(ConfigParseError): + load_config("match-host-no-arg") + + +class TestMatchOriginalHost(object): + def test_matches_target_host_not_hostname(self): + result = load_config("match-orighost").lookup("target") + assert result["hostname"] == "bogus" + assert result["user"] == "tuon" + + def test_matches_target_host_not_canonicalized_name(self, socket): + result = load_config("match-orighost-canonical").lookup("www") + assert result["hostname"] == "www.paramiko.org" + assert result["user"] == "tuon" + + def test_may_be_globbed(self): + result = load_config("match-orighost").lookup("whatever") + assert result["user"] == "matrim" + + def test_may_be_comma_separated_list(self): + for target in ("comma", "separated"): + result = load_config("match-orighost").lookup(target) + assert result["user"] == "chameleon" + + def test_comma_separated_list_may_have_internal_negation(self): + result = load_config("match-orighost").lookup("nope") + assert "user" not in result + + def test_may_be_negated(self): + result = load_config("match-orighost").lookup("docs") + assert result["user"] == "thom" + + def test_requires_an_argument(self): + with raises(ConfigParseError): + load_config("match-orighost-no-arg") + + +class TestMatchUser(object): + def test_matches_configured_username(self): + result = load_config("match-user-explicit").lookup("anything") + assert result["hostname"] == "dumb" + + @patch("paramiko.config.getpass.getuser") + def test_matches_local_username_by_default(self, getuser): + getuser.return_value = "gandalf" + result = load_config("match-user").lookup("anything") + assert result["hostname"] == "gondor" + + @patch("paramiko.config.getpass.getuser") + def test_may_be_globbed(self, getuser): + for user in ("bilbo", "bombadil"): + getuser.return_value = user + result = load_config("match-user").lookup("anything") + assert result["hostname"] == "shire" + + @patch("paramiko.config.getpass.getuser") + def test_may_be_comma_separated_list(self, getuser): + for user in ("aragorn", "frodo"): + getuser.return_value = user + result = load_config("match-user").lookup("anything") + assert result["hostname"] == "moria" + + @patch("paramiko.config.getpass.getuser") + def test_comma_separated_list_may_have_internal_negation(self, getuser): + getuser.return_value = "legolas" + result = load_config("match-user").lookup("anything") + assert "port" not in result + getuser.return_value = "gimli" + result = load_config("match-user").lookup("anything") + assert result["port"] == "7373" + + @patch("paramiko.config.getpass.getuser") + def test_may_be_negated(self, getuser): + getuser.return_value = "saruman" + result = load_config("match-user").lookup("anything") + assert result["hostname"] == "mordor" + + def test_requires_an_argument(self): + with raises(ConfigParseError): + load_config("match-user-no-arg") + + +# NOTE: highly derivative of previous suite due to the former's use of +# localuser fallback. Doesn't seem worth conflating/refactoring right now. +class TestMatchLocalUser(object): + @patch("paramiko.config.getpass.getuser") + def test_matches_local_username(self, getuser): + getuser.return_value = "gandalf" + result = load_config("match-localuser").lookup("anything") + assert result["hostname"] == "gondor" + + @patch("paramiko.config.getpass.getuser") + def test_may_be_globbed(self, getuser): + for user in ("bilbo", "bombadil"): + getuser.return_value = user + result = load_config("match-localuser").lookup("anything") + assert result["hostname"] == "shire" + + @patch("paramiko.config.getpass.getuser") + def test_may_be_comma_separated_list(self, getuser): + for user in ("aragorn", "frodo"): + getuser.return_value = user + result = load_config("match-localuser").lookup("anything") + assert result["hostname"] == "moria" + + @patch("paramiko.config.getpass.getuser") + def test_comma_separated_list_may_have_internal_negation(self, getuser): + getuser.return_value = "legolas" + result = load_config("match-localuser").lookup("anything") + assert "port" not in result + getuser.return_value = "gimli" + result = load_config("match-localuser").lookup("anything") + assert result["port"] == "7373" + + @patch("paramiko.config.getpass.getuser") + def test_may_be_negated(self, getuser): + getuser.return_value = "saruman" + result = load_config("match-localuser").lookup("anything") + assert result["hostname"] == "mordor" + + def test_requires_an_argument(self): + with raises(ConfigParseError): + load_config("match-localuser-no-arg") + + +class TestComplexMatching(object): + # NOTE: this is still a cherry-pick of a few levels of complexity, there's + # no point testing literally all possible combinations. + + def test_originalhost_host(self): + result = load_config("match-complex").lookup("target") + assert result["hostname"] == "bogus" + assert result["user"] == "rand" + + @patch("paramiko.config.getpass.getuser") + def test_originalhost_localuser(self, getuser): + getuser.return_value = "rando" + result = load_config("match-complex").lookup("remote") + assert result["user"] == "calrissian" + + @patch("paramiko.config.getpass.getuser") + def test_everything_but_all(self, getuser): + getuser.return_value = "rando" + result = load_config("match-complex").lookup("www") + assert result["port"] == "7777" + + @patch("paramiko.config.getpass.getuser") + def test_everything_but_all_with_some_negated(self, getuser): + getuser.return_value = "rando" + result = load_config("match-complex").lookup("docs") + assert result["port"] == "1234" + + def test_negated_canonical(self, socket): + # !canonical in a config that is not canonicalized - does match + result = load_config("match-canonical-no").lookup("specific") + assert result["user"] == "overload" + # !canonical in a config that is canonicalized - does NOT match + result = load_config("match-canonical-yes").lookup("www") + assert result["user"] == "hidden" diff --git a/tests/test_dss_openssh.key b/tests/test_dss_openssh.key new file mode 100644 index 00000000..2a9f8922 --- /dev/null +++ b/tests/test_dss_openssh.key @@ -0,0 +1,22 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABAsyq4pxL +R5sOprPDHGpvzxAAAAEAAAAAEAAAGxAAAAB3NzaC1kc3MAAACBAL8XEx7F9xuwBNles+vW +pNF+YcofrBhjX1r5QhpBe0eoYWLHRcroN6lxwCdGYRfgOoRjTncBiixQX/uUxAY96zDh3i +r492s2BcJt4ihvNn/AY0I0OTuX/2IwGk9CGzafjaeZNVYxMa8lcVt0hSOTjkPQ7gVuk6bJ +zMInvie+VWKLAAAAFQDUgYdY+rhR0SkKbC09BS/SIHcB+wAAAIB44+4zpCNcd0CGvZlowH +99zyPX8uxQtmTLQFuR2O8O0FgVVuCdDgD0D9W8CLOp32oatpM0jyyN89EdvSWzjHzZJ+L6 +H1FtZps7uhpDFWHdva1R25vyGecLMUuXjo5t/D7oCDih+HwHoSAxoi0QvsPd8/qqHQVznN +JKtR6thUpXEwAAAIAG4DCBjbgTTgpBw0egRkJwBSz0oTt+1IcapNU2jA6N8urMSk9YXHEQ +HKN68BAF3YJ59q2Ujv3LOXmBqGd1T+kzwUszfMlgzq8MMu19Yfzse6AIK1Agn1Vj6F7YXL +sXDN+T4KszX5+FJa7t/Zsp3nALWy6l0f4WKivEF5Y2QpEFcQAAAgCH6XUl1hYWB6kgCSHV +a4C+vQHrgFNgNwEQnE074LXHXlAhxC+Dm8XTGqVPX1KRPWzadq9/+v6pqLFqiRueB86uRb +J5WtAbUs3WwxAaC5Mi+mn42MBfL9PIwWPWCvstrAq9Nyj3EBMeX3XFLxN3RuGXIQnY/5rF +f5hriUVxhWDQGIVbBKhkpn7Geqg6nLpn7iqQhzFmFGjPmAdrllgdVGJRLyIN6BRsaltDdy +vxufkvGzKudvQ85QvsaoFJQ6K1d0S7907pexvxmWpcO7zchXb6i09BITWOAKIcHpVkbNQw ++8pzSdpggsAwCRbfk/Jkezz8sXVUCfmmJ23NFUw04/0ZbilCADRsUaPfafgVPeDznBnuCm +tfXa4JSrVUvPdwoex3SKZmYsFXwsuOEQnFkhUGHfWwTbmOmxzy6dtC24KYhnWG5OGFVJXh +3B8jQJGGs2ANfusI/Z0o15tAnQy5fqsLf9TT3RX7RG2ujIiDBsU+A1g//IXmSxxkUOQMZs +v+cMI8KfODAXmQtB30+yAgoV03Zb/bdptv+HqPT4eeecstJUxzEGYADt1mDq3uV7fQbNmo +80bppU52JjztrJb7hBmXsXHPRRK6spQ1FCatqvu1ggZeXZpEifNsHeqCljt87ueXsQsORY +pvhLzjTbTKZmjLDPuB+GxUNLEKh1ZNyAqKng== +-----END OPENSSH PRIVATE KEY----- diff --git a/tests/test_ecdsa_384_openssh.key b/tests/test_ecdsa_384_openssh.key new file mode 100644 index 00000000..8a160ce2 --- /dev/null +++ b/tests/test_ecdsa_384_openssh.key @@ -0,0 +1,11 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABDwIHkBEZ +75XuqQS6/7daAIAAAAEAAAAAEAAACIAAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlz +dHAzODQAAABhBIch5LXTq/L/TWsTGG6dIktxD8DIMh7EfvoRmWsks6CuNDTvFvbQNtY4QO +1mn5OXegHbS0M5DPIS++wpKGFP3suDEH08O35vZQasLNrL0tO2jyyEnzB2ZEx3PPYci811 +ygAAAOBKGxFl+JcMHjldOdTA9iwv88gxoelCwln/NATglUuyzHMLJwx53n8NLqrnHALvbz +RHjyTmjU4dbSM9o9Vjhcvq+1aipjAQg2qx825f7T4BMoKyhLBS/qTg7RfyW/h0Sbequ1wl +PhBfwhv0LUphRFsGdnOgrXWfZqWqxOP1WhJWIh1p+ja5va/Ii/+hD6RORQjvzbHTPJA53c +OguISImkx0vdqPuFTLyclaC3eO4Px68Ki0b8cdyivExbAWLkNOtBdIAgeO7Egbruu4O5Sn +I6bn1Kc+kZlWtO02IkwSA5DaKw== +-----END OPENSSH PRIVATE KEY----- diff --git a/tests/test_gssapi.py b/tests/test_gssapi.py index 30ffb56d..308caa93 100644 --- a/tests/test_gssapi.py +++ b/tests/test_gssapi.py @@ -204,13 +204,13 @@ class GSSAPITest(KerberosTestCase): c_token = token[0].Buffer self.assertNotEquals(0, error) - def test_2_gssapi_sspi_client(self): + def test_gssapi_sspi_client(self): """ Test the used methods of python-gssapi or sspi, sspicon from pywin32. """ self._gssapi_sspi_test() - def test_3_gssapi_sspi_server(self): + def test_gssapi_sspi_server(self): """ Test the used methods of python-gssapi or sspi, sspicon from pywin32. """ diff --git a/tests/test_pkey.py b/tests/test_pkey.py index 0e60126a..18c27bbe 100644 --- a/tests/test_pkey.py +++ b/tests/test_pkey.py @@ -29,6 +29,9 @@ from hashlib import md5 from paramiko import RSAKey, DSSKey, ECDSAKey, Ed25519Key, Message, util from paramiko.py3compat import StringIO, byte_chr, b, bytes, PY2 +from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateNumbers +from mock import patch + from .util import _support @@ -38,6 +41,11 @@ PUB_DSS = "ssh-dss AAAAB3NzaC1kc3MAAACBAOeBpgNnfRzr/twmAQRu2XwWAp3CFtrVnug6s6fgw PUB_ECDSA_256 = "ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBJSPZm3ZWkvk/Zx8WP+fZRZ5/NBBHnGQwR6uIC6XHGPDIHuWUzIjAwA0bzqkOUffEsbLe+uQgKl5kbc/L8KA/eo=" # noqa PUB_ECDSA_384 = "ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBBbGibQLW9AAZiGN2hEQxWYYoFaWKwN3PKSaDJSMqmIn1Z9sgRUuw8Y/w502OGvXL/wFk0i2z50l3pWZjD7gfMH7gX5TUiCzwrQkS+Hn1U2S9aF5WJp0NcIzYxXw2r4M2A==" # noqa PUB_ECDSA_521 = "ecdsa-sha2-nistp521 AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBACaOaFLZGuxa5AW16qj6VLypFbLrEWrt9AZUloCMefxO8bNLjK/O5g0rAVasar1TnyHE9qj4NwzANZASWjQNbc4MAG8vzqezFwLIn/kNyNTsXNfqEko9OgHZknlj2Z79dwTJcRAL4QLcT5aND0EHZLB2fAUDXiWIb2j4rg1mwPlBMiBXA==" # noqa +PUB_RSA_2K_OPENSSH = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDF+Dpr54DX0WdeTDpNAMdkCWEkl3OXtNgf58qlN1gX572OLBqLf0zT4bHstUEpU3piazph/rSWcUMuBoD46tZ6jiH7H9b9Pem2eYQWaELDDkM+v9BMbEy5rMbFRLol5OtEvPFqneyEAanPOgvd8t3yyhSev9QVusakzJ8j8LGgrA8huYZ+Srnw0shEWLG70KUKCh3rG0QIvA8nfhtUOisr2Gp+F0YxMGb5gwBlQYAYE5l6u1SjZ7hNjyNosjK+wRBFgFFBYVpkZKJgWoK9w4ijFyzMZTucnZMqKOKAjIJvHfKBf2/cEfYxSq1EndqTqjYsd9T7/s2vcn1OH5a0wkER" # noqa +RSA_2K_OPENSSH_P = 161773687847617758886803946572654778625119997081005961935077336594287351354258259920334554906235187683459069634729972458348855793639393524799865799559575414247668746919721196359908321800753913350455861871582087986355637886875933045224711827701526739934602161222599672381604211130651397331775901258858869418853 # noqa +RSA_2K_OPENSSH_Q = 154483416325630619558401349033571772244816915504195060221073502923720741119664820208064202825686848103224453777955988437823797692957091438442833606009978046057345917301441832647551208158342812551003395417862260727795454409459089912659057393394458150862012620127030757893820711157099494238156383382454310199869 # noqa +PUB_DSS_1K_OPENSSH = "ssh-dss AAAAB3NzaC1kc3MAAACBAL8XEx7F9xuwBNles+vWpNF+YcofrBhjX1r5QhpBe0eoYWLHRcroN6lxwCdGYRfgOoRjTncBiixQX/uUxAY96zDh3ir492s2BcJt4ihvNn/AY0I0OTuX/2IwGk9CGzafjaeZNVYxMa8lcVt0hSOTjkPQ7gVuk6bJzMInvie+VWKLAAAAFQDUgYdY+rhR0SkKbC09BS/SIHcB+wAAAIB44+4zpCNcd0CGvZlowH99zyPX8uxQtmTLQFuR2O8O0FgVVuCdDgD0D9W8CLOp32oatpM0jyyN89EdvSWzjHzZJ+L6H1FtZps7uhpDFWHdva1R25vyGecLMUuXjo5t/D7oCDih+HwHoSAxoi0QvsPd8/qqHQVznNJKtR6thUpXEwAAAIAG4DCBjbgTTgpBw0egRkJwBSz0oTt+1IcapNU2jA6N8urMSk9YXHEQHKN68BAF3YJ59q2Ujv3LOXmBqGd1T+kzwUszfMlgzq8MMu19Yfzse6AIK1Agn1Vj6F7YXLsXDN+T4KszX5+FJa7t/Zsp3nALWy6l0f4WKivEF5Y2QpEFcQ==" # noqa +PUB_EC_384_OPENSSH = "ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBIch5LXTq/L/TWsTGG6dIktxD8DIMh7EfvoRmWsks6CuNDTvFvbQNtY4QO1mn5OXegHbS0M5DPIS++wpKGFP3suDEH08O35vZQasLNrL0tO2jyyEnzB2ZEx3PPYci811yg==" # noqa FINGER_RSA = "1024 60:73:38:44:cb:51:86:65:7f:de:da:a2:2b:5a:57:d5" FINGER_DSS = "1024 44:78:f0:b9:a2:3c:c5:18:20:09:ff:75:5b:c1:d2:6c" @@ -45,6 +53,9 @@ FINGER_ECDSA_256 = "256 25:19:eb:55:e6:a1:47:ff:4f:38:d2:75:6f:a5:d5:60" FINGER_ECDSA_384 = "384 c1:8d:a0:59:09:47:41:8e:a8:a6:07:01:29:23:b4:65" FINGER_ECDSA_521 = "521 44:58:22:52:12:33:16:0e:ce:0e:be:2c:7c:7e:cc:1e" SIGNED_RSA = "20:d7:8a:31:21:cb:f7:92:12:f2:a4:89:37:f5:78:af:e6:16:b6:25:b9:97:3d:a2:cd:5f:ca:20:21:73:4c:ad:34:73:8f:20:77:28:e2:94:15:08:d8:91:40:7a:85:83:bf:18:37:95:dc:54:1a:9b:88:29:6c:73:ca:38:b4:04:f1:56:b9:f2:42:9d:52:1b:29:29:b4:4f:fd:c9:2d:af:47:d2:40:76:30:f3:63:45:0c:d9:1d:43:86:0f:1c:70:e2:93:12:34:f3:ac:c5:0a:2f:14:50:66:59:f1:88:ee:c1:4a:e9:d1:9c:4e:46:f0:0e:47:6f:38:74:f1:44:a8" # noqa +FINGER_RSA_2K_OPENSSH = "2048 68:d1:72:01:bf:c0:0c:66:97:78:df:ce:75:74:46:d6" +FINGER_DSS_1K_OPENSSH = "1024 cf:1d:eb:d7:61:d3:12:94:c6:c0:c6:54:35:35:b0:82" +FINGER_EC_384_OPENSSH = "384 72:14:df:c1:9a:c3:e6:0e:11:29:d6:32:18:7b:ea:9b" RSA_PRIVATE_OUT = """\ -----BEGIN RSA PRIVATE KEY----- @@ -437,6 +448,54 @@ class KeyTest(unittest.TestCase): pub = ECDSAKey(data=key.asbytes()) self.assertTrue(pub.verify_ssh_sig(b"ice weasels", msg)) + def test_load_openssh_format_RSA_key(self): + key = RSAKey.from_private_key_file( + _support("test_rsa_openssh.key"), b"television" + ) + self.assertEqual("ssh-rsa", key.get_name()) + self.assertEqual(PUB_RSA_2K_OPENSSH.split()[1], key.get_base64()) + self.assertEqual(2048, key.get_bits()) + exp_rsa = b(FINGER_RSA_2K_OPENSSH.split()[1].replace(":", "")) + my_rsa = hexlify(key.get_fingerprint()) + self.assertEqual(exp_rsa, my_rsa) + + def test_loading_openssh_RSA_keys_uses_correct_p_q(self): + # Re #1723 - not the most elegant test but given how deep it is... + with patch( + "paramiko.rsakey.rsa.RSAPrivateNumbers", wraps=RSAPrivateNumbers + ) as spy: + # Load key + RSAKey.from_private_key_file( + _support("test_rsa_openssh.key"), b"television" + ) + # Ensure spy saw the correct P and Q values as derived from + # hardcoded test private key value + kwargs = spy.call_args[1] + assert kwargs["p"] == RSA_2K_OPENSSH_P + assert kwargs["q"] == RSA_2K_OPENSSH_Q + + def test_load_openssh_format_DSS_key(self): + key = DSSKey.from_private_key_file( + _support("test_dss_openssh.key"), b"television" + ) + self.assertEqual("ssh-dss", key.get_name()) + self.assertEqual(PUB_DSS_1K_OPENSSH.split()[1], key.get_base64()) + self.assertEqual(1024, key.get_bits()) + exp_rsa = b(FINGER_DSS_1K_OPENSSH.split()[1].replace(":", "")) + my_rsa = hexlify(key.get_fingerprint()) + self.assertEqual(exp_rsa, my_rsa) + + def test_load_openssh_format_EC_key(self): + key = ECDSAKey.from_private_key_file( + _support("test_ecdsa_384_openssh.key"), b"television" + ) + self.assertEqual("ecdsa-sha2-nistp384", key.get_name()) + self.assertEqual(PUB_EC_384_OPENSSH.split()[1], key.get_base64()) + self.assertEqual(384, key.get_bits()) + exp_fp = b(FINGER_EC_384_OPENSSH.split()[1].replace(":", "")) + my_fp = hexlify(key.get_fingerprint()) + self.assertEqual(exp_fp, my_fp) + def test_salt_size(self): # Read an existing encrypted private key file_ = _support("test_rsa_password.key") @@ -455,6 +514,10 @@ class KeyTest(unittest.TestCase): finally: os.remove(newfile) + def test_load_openssh_format_RSA_nopad(self): + # check just not exploding with 'Invalid key' + RSAKey.from_private_key_file(_support("test_rsa_openssh_nopad.key")) + def test_stringification(self): key = RSAKey.from_private_key_file(_support("test_rsa.key")) comparable = TEST_KEY_BYTESTR_2 if PY2 else TEST_KEY_BYTESTR_3 diff --git a/tests/test_proxy.py b/tests/test_proxy.py new file mode 100644 index 00000000..2e0b0e51 --- /dev/null +++ b/tests/test_proxy.py @@ -0,0 +1,144 @@ +import signal +import socket + +from mock import patch +from pytest import raises + +from paramiko import ProxyCommand, ProxyCommandFailure + + +class TestProxyCommand(object): + @patch("paramiko.proxy.subprocess") + def test_init_takes_command_string(self, subprocess): + ProxyCommand(command_line="do a thing") + subprocess.Popen.assert_called_once_with( + ["do", "a", "thing"], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + bufsize=0, + ) + + @patch("paramiko.proxy.subprocess.Popen") + def test_send_writes_to_process_stdin_returning_length(self, Popen): + proxy = ProxyCommand("hi") + written = proxy.send(b"data") + Popen.return_value.stdin.write.assert_called_once_with(b"data") + assert written == len(b"data") + + @patch("paramiko.proxy.subprocess.Popen") + def test_send_raises_ProxyCommandFailure_on_error(self, Popen): + Popen.return_value.stdin.write.side_effect = IOError(0, "whoops") + with raises(ProxyCommandFailure) as info: + ProxyCommand("hi").send("data") + assert info.value.command == "hi" + assert info.value.error == "whoops" + + @patch("paramiko.proxy.subprocess.Popen") + @patch("paramiko.proxy.os.read") + @patch("paramiko.proxy.select") + def test_recv_reads_from_process_stdout_returning_bytes( + self, select, os_read, Popen + ): + stdout = Popen.return_value.stdout + select.return_value = [stdout], None, None + fileno = stdout.fileno.return_value + # Intentionally returning <5 at a time sometimes + os_read.side_effect = [b"was", b"te", b"of ti", b"me"] + proxy = ProxyCommand("hi") + data = proxy.recv(5) + assert data == b"waste" + assert [x[0] for x in os_read.call_args_list] == [ + (fileno, 5), + (fileno, 2), + ] + + @patch("paramiko.proxy.subprocess.Popen") + @patch("paramiko.proxy.os.read") + @patch("paramiko.proxy.select") + def test_recv_returns_buffer_on_timeout_if_any_read( + self, select, os_read, Popen + ): + stdout = Popen.return_value.stdout + select.return_value = [stdout], None, None + fileno = stdout.fileno.return_value + os_read.side_effect = [b"was", socket.timeout] + proxy = ProxyCommand("hi") + data = proxy.recv(5) + assert data == b"was" # not b"waste" + assert os_read.call_args[0] == (fileno, 2) + + @patch("paramiko.proxy.subprocess.Popen") + @patch("paramiko.proxy.os.read") + @patch("paramiko.proxy.select") + def test_recv_raises_timeout_if_nothing_read(self, select, os_read, Popen): + stdout = Popen.return_value.stdout + select.return_value = [stdout], None, None + fileno = stdout.fileno.return_value + os_read.side_effect = socket.timeout + proxy = ProxyCommand("hi") + with raises(socket.timeout): + proxy.recv(5) + assert os_read.call_args[0] == (fileno, 5) + + @patch("paramiko.proxy.subprocess.Popen") + @patch("paramiko.proxy.os.read") + @patch("paramiko.proxy.select") + def test_recv_raises_ProxyCommandFailure_on_non_timeout_error( + self, select, os_read, Popen + ): + select.return_value = [Popen.return_value.stdout], None, None + os_read.side_effect = IOError(0, "whoops") + with raises(ProxyCommandFailure) as info: + ProxyCommand("hi").recv(5) + assert info.value.command == "hi" + assert info.value.error == "whoops" + + @patch("paramiko.proxy.subprocess.Popen") + @patch("paramiko.proxy.os.kill") + def test_close_kills_subprocess(self, os_kill, Popen): + proxy = ProxyCommand("hi") + proxy.close() + os_kill.assert_called_once_with(Popen.return_value.pid, signal.SIGTERM) + + @patch("paramiko.proxy.subprocess.Popen") + def test_closed_exposes_whether_subprocess_has_exited(self, Popen): + proxy = ProxyCommand("hi") + Popen.return_value.returncode = None + assert proxy.closed is False + assert proxy._closed is False + Popen.return_value.returncode = 0 + assert proxy.closed is True + assert proxy._closed is True + + @patch("paramiko.proxy.time.time") + @patch("paramiko.proxy.subprocess.Popen") + @patch("paramiko.proxy.os.read") + @patch("paramiko.proxy.select") + def test_timeout_affects_whether_timeout_is_raised( + self, select, os_read, Popen, time + ): + stdout = Popen.return_value.stdout + select.return_value = [stdout], None, None + # Base case: None timeout means no timing out + os_read.return_value = b"meh" + proxy = ProxyCommand("yello") + assert proxy.timeout is None + # Implicit 'no raise' check + assert proxy.recv(3) == b"meh" + # Use settimeout to set timeout, and it is honored + time.side_effect = [0, 10] # elapsed > 7 + proxy = ProxyCommand("ohnoz") + proxy.settimeout(7) + assert proxy.timeout == 7 + with raises(socket.timeout): + proxy.recv(3) + + @patch("paramiko.proxy.subprocess", new=None) + @patch("paramiko.proxy.subprocess_import_error", new=ImportError("meh")) + def test_raises_subprocess_ImportErrors_at_runtime(self): + # Not an ideal test, but I don't know of a non-bad way to fake out + # module-time ImportErrors. So we mock the symptoms. Meh! + with raises(ImportError) as info: + ProxyCommand("hi!!!") + assert str(info.value) == "meh" diff --git a/tests/test_rsa_openssh.key b/tests/test_rsa_openssh.key new file mode 100644 index 00000000..6077c103 --- /dev/null +++ b/tests/test_rsa_openssh.key @@ -0,0 +1,28 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jYmMAAAAGYmNyeXB0AAAAGAAAABD0R3hOFS +FMb2SJeo5h8QPNAAAAEAAAAAEAAAEXAAAAB3NzaC1yc2EAAAADAQABAAABAQDF+Dpr54DX +0WdeTDpNAMdkCWEkl3OXtNgf58qlN1gX572OLBqLf0zT4bHstUEpU3piazph/rSWcUMuBo +D46tZ6jiH7H9b9Pem2eYQWaELDDkM+v9BMbEy5rMbFRLol5OtEvPFqneyEAanPOgvd8t3y +yhSev9QVusakzJ8j8LGgrA8huYZ+Srnw0shEWLG70KUKCh3rG0QIvA8nfhtUOisr2Gp+F0 +YxMGb5gwBlQYAYE5l6u1SjZ7hNjyNosjK+wRBFgFFBYVpkZKJgWoK9w4ijFyzMZTucnZMq +KOKAjIJvHfKBf2/cEfYxSq1EndqTqjYsd9T7/s2vcn1OH5a0wkERAAAD0JnzCJYfDeiUQ6 +9LOAb6/NnhKvFjCdBYal60MfLcLBHvzHLJvTneQ4f1Vknq8xEVmRba7SDSfwaEybP/1FsP +SGH6FNKA5gKllemgmcaUVr3wtNPtjX4WgsyHcwCRgHmOiyNrUj0OZR5wbZabHIIyirl4wa +LBz8Jb3GalKEagtyWsBKDCKHCFNzh8xmsT1SWhnC7baRyC8e3krQm9hGbNhpj6Q5AtN3ql +wBVamUp0eKxkt70mKBKI4v3DR8KqrEndeK6d0cegVEkE67fqa99a5J3uSDC8mglKrHiKEs +dU1dh/bOF/H3aFpINlRwvlZ95Opby7rG0BHgbZONq0+VUnABVzNTM5Xd5UKjjCF28CrQBf +XS6WeHeUx2zHtOmL1xdePk+Bii+SSUl3pLa4SDwX4nV95cSPx8vMm8dJEruxad6+MPoSuy +Oyho89jqUTSgC/RPejuTgrnB3WbzE5SJb+V3zMata0J1wxbNfYKG9U+VucUZhP4+jzfNqH +B/v8JqtuxnqR8NjPsK2+8wJxebL2KVNjKOm//6P3KSDsavpscGpVWOM06zUlwWCB26W3pP +X/+xO9aR5wiBteFKoJG1waziIjqhOJSmvq+I/texUKEUd/eEFNt10Ubc0zy0sRYVN8rIRJ +masQzCYuUylDzCa4ar1s4qngBZzWL2PRkPuXuhoHuT0J5no174GR6+6EAYZZhnq0tkYrhZ +Ar0tQ4CDlI235a3MPHzvABuwYuWys1tBuLAb+6Gc6CmCiQ+mhojfQUBYG5T65iRFA5UQsH +O1RLEC3yasxGcBI6d0J/fwOP/YLktNu3AeUumr0N9Xgf02DlBNwd+4GOI0LcQvl/3J8ppo +bamTppKPEZ2d32VNEO+Z6Zx5DlIVm5gDeMvIvdwap445VnhL3ZZH2NCkAcXM9+0WH+Quas +JCAMgPYiP9FzF+8Onmj2OmhgIVj/9eanhS3/GLrRC4xCvER2V7PwgB0I5qY110BPEttDyo +IvYE51kvtdW447SK7HZywJnkyw2RNm+29dvWJJwSQckUHuZkXEtmEPk0ePL3yf2NH5XYJc +pXX6Zac0KemCPIHr8l7GogE4Rb2BBTqddkegb9piz6QTAPcQnn+GuMFG06IBhUrgcMEQ8x +UOXYUUrT5HvSxWUcgaYH1nfC3bTWmDaodw8/HQKyF6c44rujO2s2NLFOCAyQMUNdhh3lfD +yHYLO7xYkP6xzzkpk+2lwBoeYdQdAwlKN/XqC8ZhBfwTdem/1hh1BpQJFbbFftWxU8gxxi +iuI+vmlsuIsxKoGCq8YXuophx62lo= +-----END OPENSSH PRIVATE KEY----- diff --git a/tests/test_rsa_openssh_nopad.key b/tests/test_rsa_openssh_nopad.key new file mode 100644 index 00000000..61ac1b19 --- /dev/null +++ b/tests/test_rsa_openssh_nopad.key @@ -0,0 +1,27 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABFwAAAAdzc2gtcn +NhAAAAAwEAAQAAAQEAnyMwWSwrbJxxQZWMJO5xR6eAA9De4t3GViqDRaQt/BgsvzZ14SUz +aOL/A370fKxhx/JLIOOGA0o5B0/ct+CL7XFqMi5r5+iA9VcIeYKKtoAkrEvRnagNW0WVWx +thTnE01g8Pb7fDqzI2cBuBNZ2vGNm2m4UTGC8/kl/0ES1V3KqA7lPlTrkTYg9L/ornvVHc +c8gEbMwx9XXVRzbWiuDE176ojrudY9CZduVSOgW+HK3rKkqLBs/91jv0zUK0oqTQBLR7E2 +V2GWPDU4BjlHTtYr0jpKOGDr1DLu4+NiD/mX+tGMdH6ehbDii0kXmOUaZjs4OxuK3XA/gi +KZLdj1jQQwAAA7iNnvAVjZ7wFQAAAAdzc2gtcnNhAAABAQCfIzBZLCtsnHFBlYwk7nFHp4 +AD0N7i3cZWKoNFpC38GCy/NnXhJTNo4v8DfvR8rGHH8ksg44YDSjkHT9y34IvtcWoyLmvn +6ID1Vwh5goq2gCSsS9GdqA1bRZVbG2FOcTTWDw9vt8OrMjZwG4E1na8Y2babhRMYLz+SX/ +QRLVXcqoDuU+VOuRNiD0v+iue9UdxzyARszDH1ddVHNtaK4MTXvqiOu51j0Jl25VI6Bb4c +resqSosGz/3WO/TNQrSipNAEtHsTZXYZY8NTgGOUdO1ivSOko4YOvUMu7j42IP+Zf60Yx0 +fp6FsOKLSReY5RpmOzg7G4rdcD+CIpkt2PWNBDAAAAAwEAAQAAAQEAnmMbn+VCYxth7fC2 +R5u6y6J+201sSUiKOwCdHxdFXX+CKd4+fRPVkzM6tXQKSnwX5jXVaKqLm4KoOArYl3q6Sl +1zYParF2plz8oL+URgYzwvQ/1CaDP29zzOZptdwgESoWrj5kF0UlPrsrDtbTvAJm+qPCe6 +1XtRPpKaDO6eYr0PM2QTElZy3mDBUBvu816LdG/ZtnB9g5UsocT5mmhpHTHdjrpwNu5TBe +ACVodDn5Fu66OlrrnQi4IPCAWKJ1YuzEkZqLhs1L3oMHACsmzrLjzW74SjY4kWTTvGiC6i +tDoycycThk9EGLGNso99Q1fe84/OZUff7aI3yK9KvLL7oQAAAIEAh2+XrJXSBx/v9E3aJH +ncgQH1snXr7LcSRqcWicHdbm8JsOTT3TkyXHGlSZ2rr/Y0u5V1ZSO6roJLrAHsDJzx0x0U +xE/5mpzhD+yIKQwnWkZFLzYEnYDFdXDMzmghUIik9AW7n9dtS8UtVFGaL6Vs2YCOuLqeT9 +nZUkm3UUZ+7QIAAACBAM23DFjQ0/Op2ri7fJA2qFBdXqoJdNHuyYEIrKbB6XaaSUz52+IB +MbccxEz3vPsHh69tZoJ+xZNbFJe9wdmbF+DQpoukHkJnzpk/pUq8LjQMzZfwv41X8zqaq4 +AOA7g27Rk8aKewhCXjhkr0hHEaSiuqIIindFaFti5sQMi2mtkXAAAAgQDGCXkpuKZK61p9 +L6G5yZSQBCgVtm0iQEbyDXWHjy/GqLtxJjqdyaRK57hXGjbzgJJraSy+sNP9uv2QOvyZvB +3XaPWwUYVQ34WyibCqqUaPiHxX7T1lZV+asbwgbmSqYtH5dUEJ8zT572mCwxnRjX63PwDo +5vBbR/qAW5lvRYsltQAAAAFh +-----END OPENSSH PRIVATE KEY----- diff --git a/tests/test_util.py b/tests/test_util.py index 465e75b8..8ce260d1 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -28,32 +28,10 @@ import unittest import paramiko import paramiko.util -from paramiko import SSHConfig -from paramiko.util import lookup_ssh_host_config as host_config, safe_string -from paramiko.py3compat import StringIO, byte_ord +from paramiko.util import safe_string +from paramiko.py3compat import byte_ord -# Note some lines in this configuration have trailing spaces on purpose -test_config_file = """\ -Host * - User robey - IdentityFile =~/.ssh/id_rsa - -# comment -Host *.example.com - \tUser bjork -Port=3333 -Host * -""" - -dont_strip_whitespace_please = "\t \t Crazy something dumb " - -test_config_file += dont_strip_whitespace_please -test_config_file += """ -Host spoo.example.com -Crazy something else -""" - test_hosts_file = """\ secure.example.com ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEA1PD6U2/TVxET6lkpKhOk5r\ 9q/kAYG6sP9f5zuUYP8i7FOFp/6ncCEbbtg/lB+A3iidyxoSWl+9jtoyyDOOVX4UIDV9G11Ml8om3\ @@ -65,150 +43,74 @@ 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("Transport" in symbols) - self.assertTrue("SSHClient" in symbols) - self.assertTrue("MissingHostKeyPolicy" in symbols) - self.assertTrue("AutoAddPolicy" in symbols) - self.assertTrue("RejectPolicy" in symbols) - self.assertTrue("WarningPolicy" in symbols) - self.assertTrue("SecurityOptions" in symbols) - self.assertTrue("SubsystemHandler" in symbols) - self.assertTrue("Channel" in symbols) - self.assertTrue("RSAKey" in symbols) - self.assertTrue("DSSKey" in symbols) - self.assertTrue("Message" in symbols) - self.assertTrue("SSHException" in symbols) - self.assertTrue("AuthenticationException" in symbols) - self.assertTrue("PasswordRequiredException" in symbols) - self.assertTrue("BadAuthenticationType" in symbols) - self.assertTrue("ChannelException" in symbols) - self.assertTrue("SFTP" in symbols) - self.assertTrue("SFTPFile" in symbols) - self.assertTrue("SFTPHandle" in symbols) - self.assertTrue("SFTPClient" in symbols) - self.assertTrue("SFTPServer" in symbols) - self.assertTrue("SFTPError" in symbols) - self.assertTrue("SFTPAttributes" in symbols) - self.assertTrue("SFTPServerInterface" in symbols) - self.assertTrue("ServerInterface" in symbols) - self.assertTrue("BufferedFile" in symbols) - self.assertTrue("Agent" in symbols) - self.assertTrue("AgentKey" in symbols) - self.assertTrue("HostKeys" in symbols) - self.assertTrue("SSHConfig" in symbols) - self.assertTrue("util" in symbols) - - def test_parse_config(self): - global test_config_file - f = StringIO(test_config_file) - config = paramiko.util.parse_ssh_config(f) - self.assertEqual( - config._config, - [ - {"host": ["*"], "config": {}}, - { - "host": ["*"], - "config": { - "identityfile": ["~/.ssh/id_rsa"], - "user": "robey", - }, - }, - { - "host": ["*.example.com"], - "config": {"user": "bjork", "port": "3333"}, - }, - {"host": ["*"], "config": {"crazy": "something dumb"}}, - { - "host": ["spoo.example.com"], - "config": {"crazy": "something else"}, - }, - ], - ) - - def test_host_config(self): - global test_config_file - f = StringIO(test_config_file) - config = paramiko.util.parse_ssh_config(f) - - for host, values in { - "irc.danger.com": { - "crazy": "something dumb", - "hostname": "irc.danger.com", - "user": "robey", - }, - "irc.example.com": { - "crazy": "something dumb", - "hostname": "irc.example.com", - "user": "robey", - "port": "3333", - }, - "spoo.example.com": { - "crazy": "something dumb", - "hostname": "spoo.example.com", - "user": "robey", - "port": "3333", - }, - }.items(): - values = dict( - values, - hostname=host, - identityfile=[os.path.expanduser("~/.ssh/id_rsa")], - ) - self.assertEqual( - paramiko.util.lookup_ssh_host_config(host, config), values - ) + for name in ( + "Agent", + "AgentKey", + "AuthenticationException", + "AutoAddPolicy", + "BadAuthenticationType", + "BufferedFile", + "Channel", + "ChannelException", + "ConfigParseError", + "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( sha1, b"ABCDEFGH", "This is my secret passphrase.", 64 ) hex = "".join(["%02x" % byte_ord(c) for c in x]) - self.assertEqual( - hex, - "9110e2f6793b69363e58173e9436b13a5a4b339005741d5c680e505f57d871347b4239f14fb5c46e857d5e100424873ba849ac699cea98d729e57b3e84378e8b", # noqa - ) + hexpected = "9110e2f6793b69363e58173e9436b13a5a4b339005741d5c680e505f57d871347b4239f14fb5c46e857d5e100424873ba849ac699cea98d729e57b3e84378e8b" # noqa + assert hex == hexpected def test_host_keys(self): with open("hostfile.temp", "w") as f: f.write(test_hosts_file) try: hostdict = paramiko.util.load_host_keys("hostfile.temp") - self.assertEqual(2, len(hostdict)) - self.assertEqual(1, len(list(hostdict.values())[0])) - self.assertEqual(1, len(list(hostdict.values())[1])) + assert 2 == len(hostdict) + assert 1 == len(list(hostdict.values())[0]) + assert 1 == len(list(hostdict.values())[1]) fp = hexlify( hostdict["secure.example.com"]["ssh-rsa"].get_fingerprint() ).upper() - self.assertEqual(b"E6684DB30E109B67B70FF1DC5C7F1363", fp) + assert b"E6684DB30E109B67B70FF1DC5C7F1363" == fp finally: os.unlink("hostfile.temp") - def test_host_config_expose_issue_33(self): - test_config_file = """ -Host www13.* - Port 22 - -Host *.example.com - Port 2222 - -Host * - Port 3333 - """ - f = StringIO(test_config_file) - config = paramiko.util.parse_ssh_config(f) - host = "www13.example.com" - self.assertEqual( - paramiko.util.lookup_ssh_host_config(host, config), - {"hostname": host, "port": "22"}, - ) - def test_eintr_retry(self): - self.assertEqual("foo", paramiko.util.retry_on_signal(lambda: "foo")) + assert "foo" == paramiko.util.retry_on_signal(lambda: "foo") # Variables that are set by raises_intr intr_errors_remaining = [3] @@ -221,8 +123,8 @@ Host * raise IOError(errno.EINTR, "file", "interrupted system call") self.assertTrue(paramiko.util.retry_on_signal(raises_intr) is None) - self.assertEqual(0, intr_errors_remaining[0]) - self.assertEqual(4, call_count[0]) + assert 0 == intr_errors_remaining[0] + assert 4 == call_count[0] def raises_ioerror_not_eintr(): raise IOError(errno.ENOENT, "file", "file not found") @@ -240,269 +142,10 @@ Host * lambda: paramiko.util.retry_on_signal(raises_other_exception), ) - def test_proxycommand_config_equals_parsing(self): - """ - ProxyCommand should not split on equals signs within the value. - """ - conf = """ -Host space-delimited - ProxyCommand foo bar=biz baz - -Host equals-delimited - ProxyCommand=foo bar=biz baz -""" - f = StringIO(conf) - config = paramiko.util.parse_ssh_config(f) - for host in ("space-delimited", "equals-delimited"): - self.assertEqual( - host_config(host, config)["proxycommand"], "foo bar=biz baz" - ) - - def test_proxycommand_interpolation(self): - """ - ProxyCommand should perform interpolation on the value - """ - config = paramiko.util.parse_ssh_config( - StringIO( - """ -Host specific - Port 37 - ProxyCommand host %h port %p lol - -Host portonly - Port 155 - -Host * - Port 25 - ProxyCommand host %h port %p -""" - ) - ) - for host, val in ( - ("foo.com", "host foo.com port 25"), - ("specific", "host specific port 37 lol"), - ("portonly", "host portonly port 155"), - ): - self.assertEqual(host_config(host, config)["proxycommand"], val) - - def test_proxycommand_tilde_expansion(self): - """ - Tilde (~) should be expanded inside ProxyCommand - """ - config = paramiko.util.parse_ssh_config( - StringIO( - """ -Host test - ProxyCommand ssh -F ~/.ssh/test_config bastion nc %h %p -""" - ) - ) - self.assertEqual( - "ssh -F %s/.ssh/test_config bastion nc test 22" - % os.path.expanduser("~"), - host_config("test", config)["proxycommand"], - ) - - def test_host_config_test_negation(self): - test_config_file = """ -Host www13.* !*.example.com - Port 22 - -Host *.example.com !www13.* - Port 2222 - -Host www13.* - Port 8080 - -Host * - Port 3333 - """ - f = StringIO(test_config_file) - config = paramiko.util.parse_ssh_config(f) - host = "www13.example.com" - self.assertEqual( - paramiko.util.lookup_ssh_host_config(host, config), - {"hostname": host, "port": "8080"}, - ) - - def test_host_config_test_proxycommand(self): - test_config_file = """ -Host proxy-with-equal-divisor-and-space -ProxyCommand = foo=bar - -Host proxy-with-equal-divisor-and-no-space -ProxyCommand=foo=bar - -Host proxy-without-equal-divisor -ProxyCommand foo=bar:%h-%p - """ - for host, values in { - "proxy-with-equal-divisor-and-space": { - "hostname": "proxy-with-equal-divisor-and-space", - "proxycommand": "foo=bar", - }, - "proxy-with-equal-divisor-and-no-space": { - "hostname": "proxy-with-equal-divisor-and-no-space", - "proxycommand": "foo=bar", - }, - "proxy-without-equal-divisor": { - "hostname": "proxy-without-equal-divisor", - "proxycommand": "foo=bar:proxy-without-equal-divisor-22", - }, - }.items(): - - f = StringIO(test_config_file) - config = paramiko.util.parse_ssh_config(f) - self.assertEqual( - paramiko.util.lookup_ssh_host_config(host, config), values - ) - - def test_host_config_test_identityfile(self): - test_config_file = """ - -IdentityFile id_dsa0 - -Host * -IdentityFile id_dsa1 - -Host dsa2 -IdentityFile id_dsa2 - -Host dsa2* -IdentityFile id_dsa22 - """ - for host, values in { - "foo": {"hostname": "foo", "identityfile": ["id_dsa0", "id_dsa1"]}, - "dsa2": { - "hostname": "dsa2", - "identityfile": ["id_dsa0", "id_dsa1", "id_dsa2", "id_dsa22"], - }, - "dsa22": { - "hostname": "dsa22", - "identityfile": ["id_dsa0", "id_dsa1", "id_dsa22"], - }, - }.items(): - - f = StringIO(test_config_file) - config = paramiko.util.parse_ssh_config(f) - self.assertEqual( - paramiko.util.lookup_ssh_host_config(host, config), values - ) - - def test_config_addressfamily_and_lazy_fqdn(self): - """ - Ensure the code path honoring non-'all' AddressFamily doesn't asplode - """ - test_config = """ -AddressFamily inet -IdentityFile something_%l_using_fqdn -""" - config = paramiko.util.parse_ssh_config(StringIO(test_config)) - assert config.lookup( - "meh" - ) # will die during lookup() if bug regresses - def test_clamp_value(self): - self.assertEqual(32768, paramiko.util.clamp_value(32767, 32768, 32769)) - self.assertEqual(32767, paramiko.util.clamp_value(32767, 32765, 32769)) - self.assertEqual(32769, paramiko.util.clamp_value(32767, 32770, 32769)) - - def test_config_dos_crlf_succeeds(self): - config_file = StringIO("host abcqwerty\r\nHostName 127.0.0.1\r\n") - config = paramiko.SSHConfig() - config.parse(config_file) - self.assertEqual(config.lookup("abcqwerty")["hostname"], "127.0.0.1") - - def test_get_hostnames(self): - f = StringIO(test_config_file) - config = paramiko.util.parse_ssh_config(f) - self.assertEqual( - config.get_hostnames(), {"*", "*.example.com", "spoo.example.com"} - ) - - def test_quoted_host_names(self): - test_config_file = """\ -Host "param pam" param "pam" - Port 1111 - -Host "param2" - Port 2222 - -Host param3 parara - Port 3333 - -Host param4 "p a r" "p" "par" para - Port 4444 -""" - res = { - "param pam": {"hostname": "param pam", "port": "1111"}, - "param": {"hostname": "param", "port": "1111"}, - "pam": {"hostname": "pam", "port": "1111"}, - "param2": {"hostname": "param2", "port": "2222"}, - "param3": {"hostname": "param3", "port": "3333"}, - "parara": {"hostname": "parara", "port": "3333"}, - "param4": {"hostname": "param4", "port": "4444"}, - "p a r": {"hostname": "p a r", "port": "4444"}, - "p": {"hostname": "p", "port": "4444"}, - "par": {"hostname": "par", "port": "4444"}, - "para": {"hostname": "para", "port": "4444"}, - } - f = StringIO(test_config_file) - config = paramiko.util.parse_ssh_config(f) - for host, values in res.items(): - assert paramiko.util.lookup_ssh_host_config(host, config) == values - - def test_quoted_params_in_config(self): - test_config_file = """\ -Host "param pam" param "pam" - IdentityFile id_rsa - -Host "param2" - IdentityFile "test rsa key" - -Host param3 parara - IdentityFile id_rsa - IdentityFile "test rsa key" -""" - res = { - "param pam": {"hostname": "param pam", "identityfile": ["id_rsa"]}, - "param": {"hostname": "param", "identityfile": ["id_rsa"]}, - "pam": {"hostname": "pam", "identityfile": ["id_rsa"]}, - "param2": {"hostname": "param2", "identityfile": ["test rsa key"]}, - "param3": { - "hostname": "param3", - "identityfile": ["id_rsa", "test rsa key"], - }, - "parara": { - "hostname": "parara", - "identityfile": ["id_rsa", "test rsa key"], - }, - } - f = StringIO(test_config_file) - config = paramiko.util.parse_ssh_config(f) - for host, values in res.items(): - assert paramiko.util.lookup_ssh_host_config(host, config) == values - - def test_quoted_host_in_config(self): - conf = SSHConfig() - correct_data = { - "param": ["param"], - '"param"': ["param"], - "param pam": ["param", "pam"], - '"param" "pam"': ["param", "pam"], - '"param" pam': ["param", "pam"], - 'param "pam"': ["param", "pam"], - 'param "pam" p': ["param", "pam", "p"], - '"param" pam "p"': ["param", "pam", "p"], - '"pa ram"': ["pa ram"], - '"pa ram" pam': ["pa ram", "pam"], - 'param "p a m"': ["param", "p a m"], - } - incorrect_data = ['param"', '"param', 'param "pam', 'param "pam" "p a'] - for host, values in correct_data.items(): - assert conf._get_hosts(host) == values - for host in incorrect_data: - self.assertRaises(Exception, conf._get_hosts, host) + assert 32768 == paramiko.util.clamp_value(32767, 32768, 32769) + assert 32767 == paramiko.util.clamp_value(32767, 32765, 32769) + assert 32769 == paramiko.util.clamp_value(32767, 32770, 32769) def test_safe_string(self): vanilla = b"vanilla" @@ -515,54 +158,3 @@ Host param3 parara assert safe_vanilla == vanilla, msg msg = err.format(safe_has_bytes, expected_bytes) assert safe_has_bytes == expected_bytes, msg - - def test_proxycommand_none_issue_418(self): - test_config_file = """ -Host proxycommand-standard-none - ProxyCommand None - -Host proxycommand-with-equals-none - ProxyCommand=None - """ - for host, values in { - "proxycommand-standard-none": { - "hostname": "proxycommand-standard-none" - }, - "proxycommand-with-equals-none": { - "hostname": "proxycommand-with-equals-none" - }, - }.items(): - - f = StringIO(test_config_file) - config = paramiko.util.parse_ssh_config(f) - self.assertEqual( - paramiko.util.lookup_ssh_host_config(host, config), values - ) - - def test_proxycommand_none_masking(self): - # Re: https://github.com/paramiko/paramiko/issues/670 - source_config = """ -Host specific-host - ProxyCommand none - -Host other-host - ProxyCommand other-proxy - -Host * - ProxyCommand default-proxy -""" - config = paramiko.SSHConfig() - config.parse(StringIO(source_config)) - # When bug is present, the full stripping-out of specific-host's - # ProxyCommand means it actually appears to pick up the default - # ProxyCommand value instead, due to cascading. It should (for - # backwards compatibility reasons in 1.x/2.x) appear completely blank, - # as if the host had no ProxyCommand whatsoever. - # Threw another unrelated host in there just for sanity reasons. - self.assertFalse("proxycommand" in config.lookup("specific-host")) - self.assertEqual( - config.lookup("other-host")["proxycommand"], "other-proxy" - ) - self.assertEqual( - config.lookup("some-random-host")["proxycommand"], "default-proxy" - ) diff --git a/tests/util.py b/tests/util.py index cdc835c9..9057f516 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", name) needs_gssapi = pytest.mark.skipif( |