# Copyright (c) 2015-2021 by Ron Frederick and others. # # This program and the accompanying materials are made available under # the terms of the Eclipse Public License v2.0 which accompanies this # distribution and is available at: # # http://www.eclipse.org/legal/epl-2.0/ # # This program may also be made available under the following secondary # licenses when the conditions for such availability set forth in the # Eclipse Public License v2.0 are satisfied: # # GNU General Public License, Version 2.0, or any later versions of # that license # # SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-or-later # # Contributors: # Ron Frederick - initial implementation, API, and documentation """Pattern matching for principal and host names""" from fnmatch import fnmatch from typing import Union from .misc import IPAddress, ip_network _HostPattern = Union['WildcardHostPattern', 'CIDRHostPattern'] _AnyPattern = Union['WildcardPattern', _HostPattern] class _BaseWildcardPattern: """A base class for matching '*' and '?' wildcards""" def __init__(self, pattern: str): # We need to escape square brackets in host patterns if we # want to use Python's fnmatch. self._pattern = ''.join('[[]' if ch == '[' else '[]]' if ch == ']' else ch for ch in pattern) def _matches(self, value: str) -> bool: """Return whether a wild card pattern matches a value""" return fnmatch(value, self._pattern) class WildcardPattern(_BaseWildcardPattern): """A pattern matcher for '*' and '?' wildcards""" def matches(self, value: str) -> bool: """Return whether a wild card pattern matches a value""" return super()._matches(value) class WildcardHostPattern(_BaseWildcardPattern): """Match a host name or address against a wildcard pattern""" def matches(self, host: str, addr: str, _ip: IPAddress) -> bool: """Return whether a host or address matches a wild card host pattern""" return (bool(host) and super()._matches(host)) or \ (bool(addr) and super()._matches(addr)) class CIDRHostPattern: """Match IPv4/v6 address against CIDR-style subnet pattern""" def __init__(self, pattern: str): self._network = ip_network(pattern) def matches(self, _host: str, _addr: str, ip: IPAddress) -> bool: """Return whether an IP address matches a CIDR address pattern""" return bool(ip) and ip in self._network class _PatternList: """Match against a list of comma-separated positive and negative patterns This class is a base class for building a pattern matcher that takes a set of comma-separated positive and negative patterns, returning `True` if one or more positive patterns match and no negative ones do. The pattern matching is done by objects returned by the build_pattern method. The arguments passed in when a match is performed will vary depending on what class build_pattern returns. """ def __init__(self, patterns: str): self._pos_patterns = [] self._neg_patterns = [] for pattern in patterns.split(','): if pattern.startswith('!'): negate = True pattern = pattern[1:] else: negate = False matcher = self.build_pattern(pattern) if negate: self._neg_patterns.append(matcher) else: self._pos_patterns.append(matcher) def build_pattern(self, pattern: str) -> _AnyPattern: """Abstract method to build a pattern object""" raise NotImplementedError def matches(self, *args) -> bool: """Match a set of values against positive & negative pattern lists""" pos_match = any(p.matches(*args) for p in self._pos_patterns) neg_match = any(p.matches(*args) for p in self._neg_patterns) return pos_match and not neg_match class WildcardPatternList(_PatternList): """Match names against wildcard patterns""" def build_pattern(self, pattern: str) -> WildcardPattern: """Build a wild card pattern""" return WildcardPattern(pattern) class HostPatternList(_PatternList): """Match host names & addresses against wildcard and CIDR patterns""" def build_pattern(self, pattern: str) -> _HostPattern: """Build a CIDR address or wild card host pattern""" try: return CIDRHostPattern(pattern) except ValueError: return WildcardHostPattern(pattern)