Skip to content

Commit

Permalink
Prevent get_hostname() from crashing of "Match" is not followed by a …
Browse files Browse the repository at this point in the history
…"Host" section in .ssh/config.

Closes paramiko#2339
  • Loading branch information
dirtyhillbilly committed Dec 26, 2023
1 parent 43980e7 commit 5a2e714
Show file tree
Hide file tree
Showing 4 changed files with 20 additions and 37 deletions.
32 changes: 10 additions & 22 deletions paramiko/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,18 +27,17 @@
import re
import shlex
import socket
from functools import partial
from hashlib import sha1
from io import StringIO
from functools import partial

invoke, invoke_import_error = None, None
try:
import invoke
except ImportError as e:
invoke_import_error = e

from .ssh_exception import CouldNotCanonicalize, ConfigParseError

from .ssh_exception import ConfigParseError, CouldNotCanonicalize

SSH_PORT = 22

Expand Down Expand Up @@ -237,13 +236,9 @@ def lookup(self, hostname):
hostname = self.canonicalize(hostname, options, domains)
# Overwrite HostName again here (this is also what OpenSSH does)
options["hostname"] = hostname
options = self._lookup(
hostname, options, canonical=True, final=True
)
options = self._lookup(hostname, options, canonical=True, final=True)
else:
options = self._lookup(
hostname, options, canonical=False, final=True
)
options = self._lookup(hostname, options, canonical=False, final=True)
return options

def _lookup(self, hostname, options=None, canonical=False, final=False):
Expand Down Expand Up @@ -272,9 +267,7 @@ def _lookup(self, hostname, options=None, canonical=False, final=False):
# when the extend() is being called.
options[key] = value[:] if value is not None else value
elif key == "identityfile":
options[key].extend(
x for x in value if x not in options[key]
)
options[key].extend(x for x in value if x not in options[key])
if final:
# Expand variables in resulting values
# (besides 'Match exec' which was already handled above)
Expand Down Expand Up @@ -329,7 +322,8 @@ def get_hostnames(self):
"""
hosts = set()
for entry in self._config:
hosts.update(entry["host"])
if "host" in entry:
hosts.update(entry["host"])
return hosts

def _pattern_matches(self, patterns, target):
Expand All @@ -339,19 +333,15 @@ def _pattern_matches(self, patterns, target):
match = False
for pattern in patterns:
# Short-circuit if target matches a negated pattern
if pattern.startswith("!") and fnmatch.fnmatch(
target, pattern[1:]
):
if pattern.startswith("!") and fnmatch.fnmatch(target, pattern[1:]):
return False
# Flag a match, but continue (in case of later negation) if regular
# match occurs
elif fnmatch.fnmatch(target, pattern):
match = True
return match

def _does_match(
self, match_list, target_hostname, canonical, final, options
):
def _does_match(self, match_list, target_hostname, canonical, final, options):
matched = []
candidates = match_list[:]
local_username = getpass.getuser()
Expand Down Expand Up @@ -388,9 +378,7 @@ def _does_match(
elif type_ == "localuser":
passed = self._pattern_matches(param, local_username)
elif type_ == "exec":
exec_cmd = self._tokenize(
options, target_hostname, "match-exec", param
)
exec_cmd = self._tokenize(options, target_hostname, "match-exec", param)
# This is the laziest spot in which we can get mad about an
# inability to import Invoke.
if invoke is None:
Expand Down
1 change: 1 addition & 0 deletions tests/configs/match-all
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
Match all
User awesome

2 changes: 2 additions & 0 deletions tests/configs/match-include
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Match all
Include ~/.ssh/config.extra
22 changes: 7 additions & 15 deletions tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,9 @@
Result = None

from unittest.mock import patch
from pytest import raises, mark, fixture

from paramiko import (
SSHConfig,
SSHConfigDict,
CouldNotCanonicalize,
ConfigParseError,
)
from paramiko import ConfigParseError, CouldNotCanonicalize, SSHConfig, SSHConfigDict
from pytest import fixture, mark, raises

from ._util import _config

Expand Down Expand Up @@ -223,9 +218,7 @@ def test_proxyjump_token_expansion(self, getpass):
)
assert config.lookup("justhost")["proxyjump"] == "jumpuser@justhost"
assert config.lookup("userhost")["proxyjump"] == "gandalf@userhost:222"
assert (
config.lookup("allcustom")["proxyjump"] == "gandalf@allcustom:22"
)
assert config.lookup("allcustom")["proxyjump"] == "gandalf@allcustom:22"

@patch("paramiko.config.getpass")
def test_controlpath_token_expansion(self, getpass, socket):
Expand Down Expand Up @@ -301,7 +294,6 @@ def test_proxycommand(self):
"proxycommand": "foo=bar:proxy-without-equal-divisor-22",
},
}.items():

assert config.lookup(host) == values

@patch("paramiko.config.getpass")
Expand Down Expand Up @@ -355,9 +347,7 @@ def test_config_addressfamily_and_lazy_fqdn(self):
IdentityFile something_%l_using_fqdn
"""
)
assert config.lookup(
"meh"
) # will die during lookup() if bug regresses
assert config.lookup("meh") # will die during lookup() if bug regresses

def test_config_dos_crlf_succeeds(self):
config = SSHConfig.from_text(
Expand Down Expand Up @@ -480,7 +470,6 @@ def test_proxycommand_none_issue_415(self):
"proxycommand": None,
},
}.items():

assert config.lookup(host) == values

def test_proxycommand_none_masking(self):
Expand Down Expand Up @@ -717,6 +706,9 @@ 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 test_match_include(self):
load_config("match-include")


def _expect(success_on):
"""
Expand Down

0 comments on commit 5a2e714

Please sign in to comment.