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