diff options
-rw-r--r-- | paramiko/config.py | 31 | ||||
-rw-r--r-- | sites/docs/api/config.rst | 3 | ||||
-rw-r--r-- | sites/www/changelog.rst | 5 | ||||
-rw-r--r-- | tests/configs/match-final | 14 | ||||
-rw-r--r-- | tests/test_config.py | 16 |
5 files changed, 60 insertions, 9 deletions
diff --git a/paramiko/config.py b/paramiko/config.py index 48bcb101..938ddc6f 100644 --- a/paramiko/config.py +++ b/paramiko/config.py @@ -235,10 +235,16 @@ class SSHConfig: 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) + options = self._lookup( + hostname, options, canonical=True, final=True + ) + else: + options = self._lookup( + hostname, options, canonical=False, final=True + ) return options - def _lookup(self, hostname, options=None, canonical=False): + def _lookup(self, hostname, options=None, canonical=False, final=False): # Init if options is None: options = SSHConfigDict() @@ -248,7 +254,11 @@ class SSHConfig: if not ( self._pattern_matches(context.get("host", []), hostname) or self._does_match( - context.get("matches", []), hostname, canonical, options + context.get("matches", []), + hostname, + canonical, + final, + options, ) ): continue @@ -263,9 +273,10 @@ class SSHConfig: options[key].extend( x for x in value if x not in options[key] ) - # Expand variables in resulting values (besides 'Match exec' which was - # already handled above) - options = self._expand_variables(options, hostname) + if final: + # Expand variables in resulting values + # (besides 'Match exec' which was already handled above) + options = self._expand_variables(options, hostname) return options def canonicalize(self, hostname, options, domains): @@ -336,7 +347,9 @@ class SSHConfig: match = True return match - def _does_match(self, match_list, target_hostname, canonical, options): + def _does_match( + self, match_list, target_hostname, canonical, final, options + ): matched = [] candidates = match_list[:] local_username = getpass.getuser() @@ -353,6 +366,8 @@ class SSHConfig: if type_ == "canonical": if self._should_fail(canonical, candidate): return False + if type_ == "final": + passed = final # The parse step ensures we only see this by itself or after # canonical, so it's also an easy hard pass. (No negation here as # that would be uh, pretty weird?) @@ -511,7 +526,7 @@ class SSHConfig: type_ = type_[1:] match["type"] = type_ # all/canonical have no params (everything else does) - if type_ in ("all", "canonical"): + if type_ in ("all", "canonical", "final"): matches.append(match) continue if not tokens: diff --git a/sites/docs/api/config.rst b/sites/docs/api/config.rst index d42de8ac..19fa6f7b 100644 --- a/sites/docs/api/config.rst +++ b/sites/docs/api/config.rst @@ -61,7 +61,8 @@ Paramiko releases) are included. A keyword by itself means no known departures. - ``Host`` - ``HostName``: used in ``%h`` :ref:`token expansion <TOKENS>` -- ``Match``: fully supported, with the following caveats: +- ``Match``: supports (canonical, final, exec, host, originalhost, user, localuser, all), with + the following caveats: - You must have the optional dependency Invoke installed; see :ref:`the installation docs <paramiko-itself>` (in brief: install diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index 9b903258..a05ab520 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -2,6 +2,11 @@ Changelog ========= +- :feature:`1907` (solves :issue:`1992`) Add support and tests for + ssh_config ``Match final …`` which is frequently used in ProxyJump + configurations to exclude the jump host from the ProxyJump Match statement. + Patch by ``@commonism``. + - :feature:`2058` (solves :issue:`1587` and possibly others) Add an explicit ``max_concurrent_prefetch_requests`` argument to `paramiko.client.SSHClient.get` and `paramiko.client.SSHClient.getfo`, allowing users to limit the number diff --git a/tests/configs/match-final b/tests/configs/match-final new file mode 100644 index 00000000..18e48a92 --- /dev/null +++ b/tests/configs/match-final @@ -0,0 +1,14 @@ +Host jump + HostName jump.example.orig + Port 1003 + +Host finally + HostName finally.example.org + Port 1001 + +Host default-port + HostName default-port.example.org + +Match final host "*.example.org" !host jump.example.org + ProxyJump jump + Port 1002 diff --git a/tests/test_config.py b/tests/test_config.py index fcb120b6..2e49aa3d 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -1030,3 +1030,19 @@ class TestComplexMatching: # !canonical in a config that is canonicalized - does NOT match result = load_config("match-canonical-yes").lookup("www") assert result["user"] == "hidden" + + +class TestFinalMatching(object): + def test_finally(self): + result = load_config("match-final").lookup("finally") + assert result["proxyjump"] == "jump" + assert result["port"] == "1001" + + def test_default_port(self): + result = load_config("match-final").lookup("default-port") + assert result["proxyjump"] == "jump" + assert result["port"] == "1002" + + def test_negated(self): + result = load_config("match-final").lookup("jump") + assert result["port"] == "1003" |