1"""Module contains classes used by the Apache Configurator."""
2import re
3from typing import Set
4
5from certbot.plugins import common
6
7
8class Addr(common.Addr):
9    """Represents an Apache address."""
10
11    def __eq__(self, other):
12        """This is defined as equivalent within Apache.
13
14        ip_addr:* == ip_addr
15
16        """
17        if isinstance(other, self.__class__):
18            return ((self.tup == other.tup) or
19                    (self.tup[0] == other.tup[0] and
20                     self.is_wildcard() and other.is_wildcard()))
21        return False
22
23    def __repr__(self):
24        return "certbot_apache._internal.obj.Addr(" + repr(self.tup) + ")"
25
26    def __hash__(self):  # pylint: disable=useless-super-delegation
27        # Python 3 requires explicit overridden for __hash__ if __eq__ or
28        # __cmp__ is overridden. See https://bugs.python.org/issue2235
29        return super().__hash__()
30
31    def _addr_less_specific(self, addr):
32        """Returns if addr.get_addr() is more specific than self.get_addr()."""
33        # pylint: disable=protected-access
34        return addr._rank_specific_addr() > self._rank_specific_addr()
35
36    def _rank_specific_addr(self):
37        """Returns numerical rank for get_addr()
38
39        :returns: 2 - FQ, 1 - wildcard, 0 - _default_
40        :rtype: int
41
42        """
43        if self.get_addr() == "_default_":
44            return 0
45        elif self.get_addr() == "*":
46            return 1
47        return 2
48
49    def conflicts(self, addr):
50        r"""Returns if address could conflict with correct function of self.
51
52        Could addr take away service provided by self within Apache?
53
54        .. note::IP Address is more important than wildcard.
55            Connection from 127.0.0.1:80 with choices of *:80 and 127.0.0.1:*
56            chooses 127.0.0.1:\*
57
58        .. todo:: Handle domain name addrs...
59
60        Examples:
61
62        =========================================  =====
63        ``127.0.0.1:\*.conflicts(127.0.0.1:443)``  True
64        ``127.0.0.1:443.conflicts(127.0.0.1:\*)``  False
65        ``\*:443.conflicts(\*:80)``                False
66        ``_default_:443.conflicts(\*:443)``        True
67        =========================================  =====
68
69        """
70        if self._addr_less_specific(addr):
71            return True
72        elif self.get_addr() == addr.get_addr():
73            if self.is_wildcard() or self.get_port() == addr.get_port():
74                return True
75        return False
76
77    def is_wildcard(self):
78        """Returns if address has a wildcard port."""
79        return self.tup[1] == "*" or not self.tup[1]
80
81    def get_sni_addr(self, port):
82        """Returns the least specific address that resolves on the port.
83
84        Examples:
85
86        - ``1.2.3.4:443`` -> ``1.2.3.4:<port>``
87        - ``1.2.3.4:*`` -> ``1.2.3.4:*``
88
89        :param str port: Desired port
90
91        """
92        if self.is_wildcard():
93            return self
94
95        return self.get_addr_obj(port)
96
97
98class VirtualHost:
99    """Represents an Apache Virtualhost.
100
101    :ivar str filep: file path of VH
102    :ivar str path: Augeas path to virtual host
103    :ivar set addrs: Virtual Host addresses (:class:`set` of
104        :class:`common.Addr`)
105    :ivar str name: ServerName of VHost
106    :ivar list aliases: Server aliases of vhost
107        (:class:`list` of :class:`str`)
108
109    :ivar bool ssl: SSLEngine on in vhost
110    :ivar bool enabled: Virtual host is enabled
111    :ivar bool modmacro: VirtualHost is using mod_macro
112    :ivar VirtualHost ancestor: A non-SSL VirtualHost this is based on
113
114    https://httpd.apache.org/docs/2.4/vhosts/details.html
115
116    .. todo:: Any vhost that includes the magic _default_ wildcard is given the
117              same ServerName as the main server.
118
119    """
120    # ?: is used for not returning enclosed characters
121    strip_name = re.compile(r"^(?:.+://)?([^ :$]*)")
122
123    def __init__(self, filep, path, addrs, ssl, enabled, name=None,
124                 aliases=None, modmacro=False, ancestor=None, node=None):
125
126        """Initialize a VH."""
127        self.filep = filep
128        self.path = path
129        self.addrs = addrs
130        self.name = name
131        self.aliases = aliases if aliases is not None else set()
132        self.ssl = ssl
133        self.enabled = enabled
134        self.modmacro = modmacro
135        self.ancestor = ancestor
136        self.node = node
137
138    def get_names(self):
139        """Return a set of all names."""
140        all_names: Set[str] = set()
141        all_names.update(self.aliases)
142        # Strip out any scheme:// and <port> field from servername
143        if self.name is not None:
144            all_names.add(VirtualHost.strip_name.findall(self.name)[0])
145
146        return all_names
147
148    def __str__(self):
149        return (
150            "File: {filename}\n"
151            "Vhost path: {vhpath}\n"
152            "Addresses: {addrs}\n"
153            "Name: {name}\n"
154            "Aliases: {aliases}\n"
155            "TLS Enabled: {tls}\n"
156            "Site Enabled: {active}\n"
157            "mod_macro Vhost: {modmacro}".format(
158                filename=self.filep,
159                vhpath=self.path,
160                addrs=", ".join(str(addr) for addr in self.addrs),
161                name=self.name if self.name is not None else "",
162                aliases=", ".join(name for name in self.aliases),
163                tls="Yes" if self.ssl else "No",
164                active="Yes" if self.enabled else "No",
165                modmacro="Yes" if self.modmacro else "No"))
166
167    def display_repr(self):
168        """Return a representation of VHost to be used in dialog"""
169        return (
170            "File: {filename}\n"
171            "Addresses: {addrs}\n"
172            "Names: {names}\n"
173            "HTTPS: {https}\n".format(
174                filename=self.filep,
175                addrs=", ".join(str(addr) for addr in self.addrs),
176                names=", ".join(self.get_names()),
177                https="Yes" if self.ssl else "No"))
178
179    def __eq__(self, other):
180        if isinstance(other, self.__class__):
181            return (self.filep == other.filep and self.path == other.path and
182                    self.addrs == other.addrs and
183                    self.get_names() == other.get_names() and
184                    self.ssl == other.ssl and
185                    self.enabled == other.enabled and
186                    self.modmacro == other.modmacro)
187
188        return False
189
190    def __hash__(self):
191        return hash((self.filep, self.path,
192                     tuple(self.addrs), tuple(self.get_names()),
193                     self.ssl, self.enabled, self.modmacro))
194
195    def conflicts(self, addrs):
196        """See if vhost conflicts with any of the addrs.
197
198        This determines whether or not these addresses would/could overwrite
199        the vhost addresses.
200
201        :param addrs: Iterable Addresses
202        :type addrs: Iterable :class:~obj.Addr
203
204        :returns: If addresses conflicts with vhost
205        :rtype: bool
206
207        """
208        for pot_addr in addrs:
209            for addr in self.addrs:
210                if addr.conflicts(pot_addr):
211                    return True
212        return False
213
214    def same_server(self, vhost, generic=False):
215        """Determines if the vhost is the same 'server'.
216
217        Used in redirection - indicates whether or not the two virtual hosts
218        serve on the exact same IP combinations, but different ports.
219        The generic flag indicates that that we're trying to match to a
220        default or generic vhost
221
222        .. todo:: Handle _default_
223
224        """
225
226        if not generic:
227            if vhost.get_names() != self.get_names():
228                return False
229
230            # If equal and set is not empty... assume same server
231            if self.name is not None or self.aliases:
232                return True
233        # If we're looking for a generic vhost,
234        # don't return one with a ServerName
235        elif self.name:
236            return False
237
238        # Both sets of names are empty.
239
240        # Make conservative educated guess... this is very restrictive
241        # Consider adding more safety checks.
242        if len(vhost.addrs) != len(self.addrs):
243            return False
244
245        # already_found acts to keep everything very conservative.
246        # Don't allow multiple ip:ports in same set.
247        already_found: Set[str] = set()
248
249        for addr in vhost.addrs:
250            for local_addr in self.addrs:
251                if (local_addr.get_addr() == addr.get_addr() and
252                        local_addr != addr and
253                        local_addr.get_addr() not in already_found):
254
255                    # This intends to make sure we aren't double counting...
256                    # e.g. 127.0.0.1:* - We require same number of addrs
257                    #  currently
258                    already_found.add(local_addr.get_addr())
259                    break
260            else:
261                return False
262
263        return True
264