Fix nginx choose_vhost to use nginx host-choosing rules

This commit is contained in:
yan
2015-04-17 22:24:18 -07:00
parent 3c806b120a
commit 7b72262811
2 changed files with 149 additions and 25 deletions
@@ -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.
+92 -1
View File
@@ -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