mirror of
https://github.com/certbot/certbot.git
synced 2026-07-27 16:30:31 +02:00
Fix nginx choose_vhost to use nginx host-choosing rules
This commit is contained in:
@@ -164,7 +164,8 @@ class NginxConfigurator(object):
|
||||
# Vhost parsing methods
|
||||
#######################
|
||||
def choose_vhost(self, target_name):
|
||||
"""Chooses a virtual host based on the given domain name.
|
||||
"""Chooses a virtual host based on the given domain name. NOTE: This
|
||||
makes the vhost SSL-enabled if it isn't already.
|
||||
|
||||
.. todo:: This should maybe return list if no obvious answer
|
||||
is presented.
|
||||
@@ -178,34 +179,66 @@ class NginxConfigurator(object):
|
||||
:rtype: :class:`~letsencrypt.client.plugins.nginx.obj.VirtualHost`
|
||||
|
||||
"""
|
||||
# Allows for domain names to be associated with a virtual host
|
||||
vhost = None
|
||||
|
||||
# If we already found the vhost for the target, use it
|
||||
if target_name in self.assoc:
|
||||
return self.assoc[target_name]
|
||||
# Check for servernames/aliases for ssl hosts
|
||||
for vhost in self.vhosts:
|
||||
if vhost.ssl and target_name in vhost.names:
|
||||
self.assoc[target_name] = vhost
|
||||
return vhost
|
||||
# Checking for domain name in vhost address
|
||||
# This technique is not recommended by Nginx but is technically valid
|
||||
target_addr = obj.Addr((target_name, "443"))
|
||||
for vhost in self.vhosts:
|
||||
if target_addr in vhost.addrs:
|
||||
self.assoc[target_name] = vhost
|
||||
return vhost
|
||||
vhost = self.assoc[target_name]
|
||||
else:
|
||||
matches = self._get_ranked_matches(target_name)
|
||||
if len(matches) == 0:
|
||||
# No matches at all :'(
|
||||
break
|
||||
elif matches[0]['rank'] in range(2, 6):
|
||||
# Wildcard match - need to find the longest one
|
||||
rank = matches[0]['rank']
|
||||
wildcards = [x for x in matches if x['rank'] == rank]
|
||||
vhost = max(wildcards, key=lambda x: len(x['name']))['vhost']
|
||||
else:
|
||||
vhost = matches[0]['vhost']
|
||||
|
||||
# Check for non ssl vhosts with servernames/aliases == "name"
|
||||
for vhost in self.vhosts:
|
||||
if not vhost.ssl and target_name in vhost.names:
|
||||
if vhost is not None:
|
||||
self.assoc[target_name] = vhost
|
||||
if not vhost.ssl:
|
||||
vhost = self._make_vhost_ssl(vhost)
|
||||
self.assoc[target_name] = vhost
|
||||
return vhost
|
||||
|
||||
# No matches, search for the default
|
||||
return vhost
|
||||
|
||||
def _get_ranked_matches(self, target_name):
|
||||
"""
|
||||
Returns a ranked list of vhosts that match target_name.
|
||||
|
||||
:param str target_name: The name to match
|
||||
:returns: list of dicts containing the vhost, the matching name, and
|
||||
the numerical rank
|
||||
:rtype: list
|
||||
|
||||
"""
|
||||
# Nginx chooses a matching server name for a request with precedence:
|
||||
# 1. exact name match
|
||||
# 2. longest wildcard name starting with *
|
||||
# 3. longest wildcard name ending with *
|
||||
# 4. first matching regex in order of appearance in the file
|
||||
matches = []
|
||||
for vhost in self.vhosts:
|
||||
if "_default_:443" in vhost.addrs:
|
||||
return vhost
|
||||
return None
|
||||
name_type, name = parser.get_best_match(target_name, vhost.names)
|
||||
if name_type == 'exact':
|
||||
matches.append({'vhost': vhost,
|
||||
'name': name,
|
||||
'rank': 0 if vhost.ssl else 1})
|
||||
elif name_type == 'wildcard_start':
|
||||
matches.append({'vhost': vhost,
|
||||
'name': name,
|
||||
'rank': 2 if vhost.ssl else 3})
|
||||
elif name_type == 'wildcard_end':
|
||||
matches.append({'vhost': vhost,
|
||||
'name': name,
|
||||
'rank': 4 if vhost.ssl else 5})
|
||||
elif name_type == 'regex':
|
||||
matches.append({'vhost': vhost,
|
||||
'name': name,
|
||||
'rank': 6 if vhost.ssl else 7})
|
||||
return sorted(matches, key=lambda x: x['rank'], reverse=True)
|
||||
|
||||
def get_all_names(self):
|
||||
"""Returns all names found in the Nginx Configuration.
|
||||
|
||||
@@ -3,6 +3,7 @@ import glob
|
||||
import logging
|
||||
import os
|
||||
import pyparsing
|
||||
import re
|
||||
|
||||
from letsencrypt.client import errors
|
||||
from letsencrypt.client.plugins.nginx import obj
|
||||
@@ -64,7 +65,7 @@ class NginxParser(object):
|
||||
|
||||
:param str path: The path
|
||||
:returns: The absolute path
|
||||
:rtype str
|
||||
:rtype: str
|
||||
|
||||
"""
|
||||
if not os.path.isabs(path):
|
||||
@@ -114,6 +115,9 @@ class NginxParser(object):
|
||||
self.abs_path(directive[1]))
|
||||
for f in included_files:
|
||||
try:
|
||||
# Assign instead of append because servers[f]
|
||||
# should be empty since server blocks cannot
|
||||
# contain other server blocks.
|
||||
servers[f] = self.parsed[f]
|
||||
except:
|
||||
pass
|
||||
@@ -279,3 +283,90 @@ def do_for_subarray(entry, condition, func):
|
||||
logging.warn("Error in do_for_subarray for %s" % item)
|
||||
else:
|
||||
do_for_subarray(item, condition, func)
|
||||
|
||||
|
||||
def get_best_match(target_name, names):
|
||||
"""Finds the best match for target_name out of names using the Nginx
|
||||
name-matching rules (exact > longest wildcard starting with * >
|
||||
longest wildcard ending with * > regex).
|
||||
|
||||
:param str target_name: The name to match
|
||||
:param list names: The candidate server names
|
||||
:returns: Tuple of (type of match, the name that matched)
|
||||
:rtype: tuple
|
||||
|
||||
"""
|
||||
exact = []
|
||||
wildcard_start = []
|
||||
wildcard_end = []
|
||||
regex = []
|
||||
|
||||
for name in names:
|
||||
if _exact_match(target_name, name):
|
||||
exact.append(name)
|
||||
elif _wildcard_match(target_name, name, True):
|
||||
wildcard_start.append(name)
|
||||
elif _wildcard_match(target_name, name, False):
|
||||
wildcard_end.append(name)
|
||||
elif _regex_match(target_name, name):
|
||||
regex.append(name)
|
||||
|
||||
if len(exact) > 0:
|
||||
# There can be more than one exact match; e.g. eff.org, .eff.org
|
||||
match = min(exact, key=lambda x: len(x))
|
||||
return ('exact', match)
|
||||
if len(wildcard_start) > 0:
|
||||
# Return the longest wildcard
|
||||
match = max(wildcard_start, key=lambda x: len(x))
|
||||
return ('wildcard_start', match)
|
||||
if len(wildcard_end) > 0:
|
||||
# Return the longest wildcard
|
||||
match = max(wildcard_end, key=lambda x: len(x))
|
||||
return ('wildcard_end', match)
|
||||
if len(regex) > 0:
|
||||
# Just return the first one for now
|
||||
match = regex[0]
|
||||
return ('regex', match)
|
||||
|
||||
return (None, None)
|
||||
|
||||
|
||||
def _exact_match(target_name, name):
|
||||
return (target_name == name or target_name == '.' + name)
|
||||
|
||||
|
||||
def _wildcard_match(target_name, name, start):
|
||||
parts = target_name.split('.')
|
||||
match_parts = name.split('.')
|
||||
|
||||
# If the domain ends in a wildcard, do the match procedure in reverse
|
||||
if not start:
|
||||
parts.reverse()
|
||||
match_parts.reverse()
|
||||
|
||||
# The first part must be a wildcard
|
||||
if match_parts.pop(0) != '*':
|
||||
return False
|
||||
|
||||
target_name = '.'.join(parts)
|
||||
name = '.'.join(match_parts)
|
||||
|
||||
# Ex: www.eff.org matches *.eff.org, eff.org does not match *.eff.org
|
||||
return target_name.endswith('.' + name)
|
||||
|
||||
|
||||
def _regex_match(target_name, name):
|
||||
# Must start with a tilde
|
||||
if name[0] != '~':
|
||||
return False
|
||||
|
||||
# After tilde is a perl-compatible regex
|
||||
try:
|
||||
regex = re.compile(name[1:])
|
||||
if regex.match(target_name):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
except:
|
||||
# perl-compatible regexes are sometimes not recognized by python
|
||||
return False
|
||||
|
||||
Reference in New Issue
Block a user