File: pattern.py

package info (click to toggle)
python-asyncssh 2.21.0-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 3,464 kB
  • sloc: python: 40,306; makefile: 11
file content (145 lines) | stat: -rw-r--r-- 4,662 bytes parent folder | download | duplicates (3)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
# Copyright (c) 2015-2021 by Ron Frederick <ronf@timeheart.net> 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)