diff options
Diffstat (limited to 'tests/test_config.py')
-rw-r--r-- | tests/test_config.py | 467 |
1 files changed, 428 insertions, 39 deletions
diff --git a/tests/test_config.py b/tests/test_config.py index cbd3f623..903690f7 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -1,74 +1,463 @@ # This file is part of Paramiko and subject to the license in /LICENSE in this # repository +from os.path import expanduser + import pytest +from pytest import raises, mark -from paramiko import config -from paramiko.util import parse_ssh_config from paramiko.py3compat import StringIO +from paramiko import SSHConfig, SSHConfigDict +from paramiko.util import lookup_ssh_host_config, parse_ssh_config -def test_SSHConfigDict_construct_empty(): - assert not config.SSHConfigDict() +# Note some lines in this configuration have trailing spaces on purpose +test_config_file = """\ +Host * + User robey + IdentityFile =~/.ssh/id_rsa -def test_SSHConfigDict_construct_from_list(): - assert config.SSHConfigDict([(1, 2)])[1] == 2 +# comment +Host *.example.com + \tUser bjork +Port=3333 +Host * +""" +dont_strip_whitespace_please = "\t \t Crazy something dumb " -def test_SSHConfigDict_construct_from_dict(): - assert config.SSHConfigDict({1: 2})[1] == 2 +test_config_file += dont_strip_whitespace_please +test_config_file += """ +Host spoo.example.com +Crazy something else +""" -@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 +class TestSSHConfig(object): + def test_parse_config(self): + global test_config_file + f = StringIO(test_config_file) + config = parse_ssh_config(f) + 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 config._config == expected + def test_host_config(self): + global test_config_file + f = StringIO(test_config_file) + config = parse_ssh_config(f) -@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 + 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=[expanduser("~/.ssh/id_rsa")], + ) + assert lookup_ssh_host_config(host, config) == values + def test_host_config_expose_fabric_issue_33(self): + test_config_file = """ +Host www13.* + Port 22 -@pytest.mark.parametrize("int_val", ("42", 42)) -def test_SSHConfigDict_as_int(int_val): - assert config.SSHConfigDict({"key": int_val}).as_int("key") == 42 +Host *.example.com + Port 2222 +Host * + Port 3333 + """ + f = StringIO(test_config_file) + config = parse_ssh_config(f) + host = "www13.example.com" + expected = {"hostname": host, "port": "22"} + assert lookup_ssh_host_config(host, config) == expected -@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_proxycommand_config_equals_parsing(self): + """ + ProxyCommand should not split on equals signs within the value. + """ + conf = """ +Host space-delimited + ProxyCommand foo bar=biz baz - try: - int(non_int) - except Exception as e: - exception_type = type(e) +Host equals-delimited + ProxyCommand=foo bar=biz baz +""" + f = StringIO(conf) + config = parse_ssh_config(f) + for host in ("space-delimited", "equals-delimited"): + value = lookup_ssh_host_config(host, config)["proxycommand"] + assert value == "foo bar=biz baz" - with pytest.raises(exception_type): - conf.as_int("key") + def test_proxycommand_interpolation(self): + """ + ProxyCommand should perform interpolation on the value + """ + config = parse_ssh_config( + StringIO( + """ +Host specific + Port 37 + ProxyCommand host %h port %p lol +Host portonly + Port 155 -def test_SSHConfig_host_dicts_are_SSHConfigDict_instances(): - test_config_file = """ -Host *.example.com +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 lookup_ssh_host_config(host, config)["proxycommand"] == val + + def test_proxycommand_tilde_expansion(self): + """ + Tilde (~) should be expanded inside ProxyCommand + """ + config = parse_ssh_config( + StringIO( + """ +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 = lookup_ssh_host_config("test", config)["proxycommand"] + assert got == expected + + 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 = parse_ssh_config(f) - assert config.lookup("foo.example.com").as_int("port") == 2222 + f = StringIO(test_config_file) + config = parse_ssh_config(f) + host = "www13.example.com" + expected = {"hostname": host, "port": "8080"} + assert lookup_ssh_host_config(host, config) == expected + def test_host_config_test_proxycommand(self): + test_config_file = """ +Host proxy-with-equal-divisor-and-space +ProxyCommand = foo=bar -def test_SSHConfig_wildcard_host_dicts_are_SSHConfigDict_instances(): - test_config_file = """\ -Host *.example.com - Port 2222 +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 = parse_ssh_config(f) + assert 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 = parse_ssh_config(f) + assert 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 = parse_ssh_config(StringIO(test_config)) + assert config.lookup( + "meh" + ) # will die during lookup() if bug regresses + + def test_config_dos_crlf_succeeds(self): + config_file = StringIO("host abcqwerty\r\nHostName 127.0.0.1\r\n") + config = SSHConfig() + config.parse(config_file) + assert config.lookup("abcqwerty")["hostname"] == "127.0.0.1" + + def test_get_hostnames(self): + f = StringIO(test_config_file) + config = parse_ssh_config(f) + expected = {"*", "*.example.com", "spoo.example.com"} + assert config.get_hostnames() == expected + + 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 = parse_ssh_config(f) + for host, values in res.items(): + assert 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 = parse_ssh_config(f) + for host, values in res.items(): + assert 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: + with raises(Exception): + conf._get_hosts(host) + + def test_proxycommand_none_issue_418(self): + test_config_file = """ +Host proxycommand-standard-none + ProxyCommand None + +Host proxycommand-with-equals-none + ProxyCommand=None """ - f = StringIO(test_config_file) - config = parse_ssh_config(f) - assert config.lookup("anything-else").as_int("port") == 3333 + 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 = parse_ssh_config(f) + assert 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 = 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. + 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" + + +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): + test_config_file = """ + Host *.example.com + Port 2222 + + Host * + Port 3333 + """ + f = StringIO(test_config_file) + config = parse_ssh_config(f) + assert config.lookup("foo.example.com").as_int("port") == 2222 + + def test_SSHConfig_wildcard_host_dicts_are_SSHConfigDict_instances(self): + test_config_file = """\ + Host *.example.com + Port 2222 + + Host * + Port 3333 + """ + f = StringIO(test_config_file) + config = parse_ssh_config(f) + assert config.lookup("anything-else").as_int("port") == 3333 |