Convert to servername/serveralias

This commit is contained in:
James Kasten 2015-07-21 17:16:46 -07:00
parent 6a8590b4cc
commit d25c7c36e7
6 changed files with 206 additions and 221 deletions

View file

@ -272,7 +272,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
best_points = 0
for vhost in self.vhosts:
if target_name in vhost.names:
if target_name in vhost.get_names():
points = 2
elif any(addr.get_addr() == target_name for addr in vhost.addrs):
points = 1
@ -312,7 +312,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
all_names = set()
for vhost in self.vhosts:
all_names.update(vhost.names)
all_names.update(vhost.get_names())
for addr in vhost.addrs:
name = self.get_name_from_ip(addr)
if name:
@ -326,7 +326,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
:param addr: IP Address
:type addr: ~.common.Addr
:returns: name
:returns: name or empty string if name cannot be determined
:rtype: str
"""
@ -347,17 +347,21 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
:type host: :class:`~letsencrypt_apache.obj.VirtualHost`
"""
name_match = self.aug.match(("%s//*[self::directive=~regexp('%s')] | "
"%s//*[self::directive=~regexp('%s')]" %
(host.path,
parser.case_i("ServerName"),
host.path,
parser.case_i("ServerAlias"))))
# Take the final ServerName as each overrides the previous
servername_match = self.parser.find_dir(
"ServerName", None, host.path, exclude=False)
serveralias_match = self.parser.find_dir(
"ServerAlias", None, host.path, exclude=False)
for name in name_match:
args = self.aug.match(name + "/*")
for arg in args:
host.add_name(self.parser.get_arg(arg))
aliases = []
for alias in serveralias_match:
aliases.append(self.parser.get_arg(alias))
if servername_match:
# Get last ServerName as each overwrites the previous
host.add_names(self.parser.get_arg(servername_match[-1]), aliases)
else:
host.add_names(None, aliases)
def _create_vhost(self, path):
"""Used by get_virtual_hosts to create vhost objects
@ -700,28 +704,25 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
if "rewrite_module" not in self.parser.modules:
self.enable_mod("rewrite")
general_v = self._general_vhost(ssl_vhost)
general_v = self._get_http_vhost(ssl_vhost)
if general_v is None:
# Add virtual_server with redirect
logger.debug(
"Did not find http version of ssl virtual host... creating")
return self._create_redirect_vhost(ssl_vhost)
logger.debug("Did not find http version of ssl virtual host "
"attempting to create")
redirect_addrs = self._get_redirect_addrs(ssl_vhost)
for vhost in self.vhosts:
if vhost.enabled and vhost.conflicts(redirect_addrs):
raise errors.PluginError(
"Unable to find corresponding HTTP vhost; "
"Unable to create one as intended addresses conflict; "
"Current configuration does not support automated "
"redirection")
self.create_redirect_vhost(redirect_addrs)
else:
# Check if redirection already exists
exists, code = self._existing_redirect(general_v)
if exists:
if code == 0:
logger.debug("Redirect already added")
logger.info(
"Configuration is already redirecting traffic to HTTPS")
return
else:
logger.info("Unknown redirect exists for this vhost")
raise errors.PluginError(
"Unknown redirect already exists "
"in {}".format(general_v.filep))
self._verify_no_redirects(vhost)
# Add directives to server
self.parser.add_dir(general_v.path, "RewriteEngine", "On")
self.parser.add_dir(general_v.path, "RewriteEngine", "on")
self.parser.add_dir(general_v.path, "RewriteRule",
constants.REWRITE_HTTPS_ARGS)
self.save_notes += ("Redirecting host in %s to ssl vhost in %s\n" %
@ -731,24 +732,16 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
logger.info("Redirecting vhost in %s to ssl vhost in %s",
general_v.filep, ssl_vhost.filep)
def _existing_redirect(self, vhost):
def _verify_no_redirects(self, vhost):
"""Checks to see if existing redirect is in place.
Checks to see if virtualhost already contains a rewrite or redirect
returns boolean, integer
The boolean indicates whether the redirection exists...
The integer has the following code:
0 - Existing letsencrypt https rewrite rule is appropriate and in place
1 - Virtual host contains a Redirect directive
2 - Virtual host contains an unknown RewriteRule
-1 is also returned in case of no redirection/rewrite directives
:param vhost: vhost to check
:type vhost: :class:`~letsencrypt_apache.obj.VirtualHost`
:returns: Success, code value... see documentation
:rtype: bool, int
:raises errors.PluginError: When another redirection exists
"""
rewrite_path = self.parser.find_dir("RewriteRule", None, vhost.path)
@ -756,20 +749,18 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
if redirect_path:
# "Existing Redirect directive for virtualhost"
return True, 1
if not rewrite_path:
raise errors.PluginError("Existing Redirect present on HTTP vhost.")
if rewrite_path:
# "No existing redirection for virtualhost"
return False, -1
if len(rewrite_path) == len(constants.REWRITE_HTTPS_ARGS):
for idx, match in enumerate(rewrite_path):
if (self.aug.get(match) !=
constants.REWRITE_HTTPS_ARGS[idx]):
# Not a letsencrypt https rewrite
return True, 2
# Existing letsencrypt https rewrite rule is in place
return True, 0
# Rewrite path exists but is not a letsencrypt https rule
return True, 2
if len(rewrite_path) != len(constants.REWRITE_HTTPS_ARGS):
raise errors.PluginError("Unknown Existing RewriteRule")
for match, arg in itertools.izip(
rewrite_path, constants.REWRITE_HTTPS_ARGS):
if self.aug.get(match) != arg:
raise errors.PluginError("Unknown Existing RewriteRule")
raise errors.PluginError(
"Let's Encrypt has already enabled redirection")
def _create_redirect_vhost(self, ssl_vhost):
"""Creates an http_vhost specifically to redirect for the ssl_vhost.
@ -782,62 +773,9 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
:rtype: tuple
"""
# Consider changing this to a dictionary check
# Make sure adding the vhost will be safe
conflict, host_or_addrs = self._conflicting_host(ssl_vhost)
if conflict:
raise errors.PluginError(
"Unable to create a redirection vhost - {}".format(
host_or_addrs))
text = self._get_redirect_config_str(ssl_vhost)
redirect_addrs = host_or_addrs
# get servernames and serveraliases
serveralias = ""
servername = ""
size_n = len(ssl_vhost.names)
if size_n > 0:
servername = "ServerName " + ssl_vhost.names[0]
if size_n > 1:
serveralias = " ".join(ssl_vhost.names[1:size_n])
serveralias = "ServerAlias " + serveralias
redirect_file = ("<VirtualHost" + redirect_addrs + ">\n"
"%s \n"
"%s \n"
"ServerSignature Off\n"
"\n"
"RewriteEngine On\n"
"RewriteRule %s\n"
"\n"
"ErrorLog /var/log/apache2/redirect.error.log\n"
"LogLevel warn\n"
"</VirtualHost>\n"
% (servername, serveralias,
" ".join(constants.REWRITE_HTTPS_ARGS)))
# Write out the file
# This is the default name
redirect_filename = "le-redirect.conf"
# See if a more appropriate name can be applied
if len(ssl_vhost.names) > 0:
# Sanity check...
# make sure servername doesn't exceed filename length restriction
if ssl_vhost.names[0] < (255-23):
redirect_filename = "le-redirect-%s.conf" % ssl_vhost.names[0]
redirect_filepath = os.path.join(
self.parser.root, "sites-available", redirect_filename)
# Register the new file that will be created
# Note: always register the creation before writing to ensure file will
# be removed in case of unexpected program exit
self.reverter.register_file_creation(False, redirect_filepath)
# Write out file
with open(redirect_filepath, "w") as redirect_fd:
redirect_fd.write(redirect_file)
logger.info("Created redirect file: %s", redirect_filename)
self._write_out_redirect(ssl_vhost, text)
self.aug.load()
# Make a new vhost data structure and add it to the lists
@ -849,85 +787,77 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
"ssl vhost %s\n" %
(new_vhost.filep, ssl_vhost.filep))
def _conflicting_host(self, ssl_vhost):
"""Checks for conflicting HTTP vhost for ssl_vhost.
def _get_redirect_config_str(self, ssl_vhost):
# get servernames and serveraliases
serveralias = ""
servername = ""
Checks for a conflicting host, such that a new port 80 host could not
be created without ruining the apache config
Used with redirection
if ssl_vhost.name is not None:
servername = "ServerName " + ssl_vhost.name
if ssl_vhost.aliases:
serveralias = "ServerAlias " + " ".join(ssl_vhost.aliases)
returns: conflict, host_or_addrs - boolean
if conflict: returns conflicting vhost
if not conflict: returns space separated list of new host addrs
return ("<VirtualHost %s>\n"
"%s \n"
"%s \n"
"ServerSignature Off\n"
"\n"
"RewriteEngine On\n"
"RewriteRule %s\n"
"\n"
"ErrorLog /var/log/apache2/redirect.error.log\n"
"LogLevel warn\n"
"</VirtualHost>\n"
% (" ".join(self._get_redirect_addrs(ssl_vhost)),
servername, serveralias,
" ".join(constants.REWRITE_HTTPS_ARGS)))
:param ssl_vhost: SSL Vhost to check for possible port 80 redirection
:type ssl_vhost: :class:`~letsencrypt_apache.obj.VirtualHost`
def _write_out_redirect(self, ssl_vhost, text):
# This is the default name
redirect_filename = "le-redirect.conf"
:returns: TODO
:rtype: TODO
# See if a more appropriate name can be applied
if ssl_vhost.name is not None:
# make sure servername doesn't exceed filename length restriction
if len(ssl_vhost.name) < (255 - (len(redirect_filename) + 1)):
redirect_filename = "le-redirect-%s.conf" % ssl_vhost.servername
"""
# Consider changing this to a dictionary check
redirect_addrs = ""
for ssl_a in ssl_vhost.addrs:
# Add space on each new addr, combine "VirtualHost"+redirect_addrs
redirect_addrs = redirect_addrs + " "
ssl_a_vhttp = ssl_a.get_addr_obj("80")
# Search for a conflicting host...
for vhost in self.vhosts:
if vhost.enabled:
if (ssl_a_vhttp in vhost.addrs or
ssl_a.get_addr_obj("") in vhost.addrs or
ssl_a.get_addr_obj("*") in vhost.addrs):
# We have found a conflicting host... just return
return True, vhost
redirect_filepath = os.path.join(
self.parser.root, "sites-available", redirect_filename)
redirect_addrs = redirect_addrs + ssl_a_vhttp
# Register the new file that will be created
# Note: always register the creation before writing to ensure file will
# be removed in case of unexpected program exit
self.reverter.register_file_creation(False, redirect_filepath)
return False, redirect_addrs
# Write out file
with open(redirect_filepath, "w") as redirect_file:
redirect_file.write(text)
logger.info("Created redirect file: %s", redirect_filename)
def _general_vhost(self, ssl_vhost):
"""Find appropriate HTTP vhost for ssl_vhost.
return redirect_filepath
Function needs to be thoroughly tested and perhaps improved
Will not do well with malformed configurations
Consider changing this into a dict check
def _get_http_vhost(self, ssl_vhost):
"""Find appropriate HTTP vhost for ssl_vhost."""
# First candidate vhosts filter
candidate_http_vhs = [
vhost for vhost in self.vhosts if not vhost.ssl
]
:param ssl_vhost: ssl vhost to check
:type ssl_vhost: :class:`~letsencrypt_apache.obj.VirtualHost`
# Second filter - check addresses
for http_vh in candidate_http_vhs:
if http_vh.same_server(ssl_vhost):
return http_vh
:returns: HTTP vhost or None if unsuccessful
:rtype: :class:`~letsencrypt_apache.obj.VirtualHost` or ``None``
"""
# _default_:443 check
# Instead... should look for vhost of the form *:80
# Should we prompt the user?
ssl_addrs = ssl_vhost.addrs
if ssl_addrs == obj.Addr.fromstring("_default_:443"):
ssl_addrs = [obj.Addr.fromstring("*:443")]
for vhost in self.vhosts:
found = 0
# Not the same vhost, and same number of addresses
if vhost != ssl_vhost and len(vhost.addrs) == len(ssl_vhost.addrs):
# Find each address in ssl_host in test_host
for ssl_a in ssl_addrs:
for test_a in vhost.addrs:
if test_a.get_addr() == ssl_a.get_addr():
# Check if found...
if (test_a.get_port() == "80" or
test_a.get_port() == "" or
test_a.get_port() == "*"):
found += 1
break
# Check to make sure all addresses were found
# and names are equal
if (found == len(ssl_vhost.addrs) and
vhost.names == ssl_vhost.names):
return vhost
return None
def _get_redirect_addrs(self, ssl_vhost):
redirects = set()
for addr in vhost.addrs:
redirects.add(addr.get_addr_obj("80"))
return redirects
def get_all_certs_keys(self):
"""Find all existing keys, certs from configuration.
@ -1075,38 +1005,6 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
self.reverter.register_file_creation(False, enabled_path)
os.symlink(os.path.join(mods_available, filename), enabled_path)
def mod_loaded(self, module):
"""Checks to see if mod_ssl is loaded
Uses ``apache_ctl`` to get loaded module list. This also effectively
serves as a config_test.
:returns: If module is loaded.
:rtype: bool
"""
try:
proc = subprocess.Popen(
[self.conf("ctl"), "-M"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
stdout, stderr = proc.communicate()
except (OSError, ValueError):
logger.error(
"Error accessing %s for loaded modules!", self.conf("ctl"))
raise errors.MisconfigurationError("Error accessing loaded modules")
# Small errors that do not impede
if proc.returncode != 0:
logger.warn("Error in checking loaded module list: %s", stderr)
raise errors.MisconfigurationError(
"Apache is unable to check whether or not the module is "
"loaded because Apache is misconfigured.")
if module in stdout:
return True
return False
def restart(self):
"""Restarts apache server.

View file

@ -20,5 +20,5 @@ MOD_SSL_CONF_SRC = pkg_resources.resource_filename(
distribution."""
REWRITE_HTTPS_ARGS = [
"^.*$", "https://%{SERVER_NAME}%{REQUEST_URI}", "[L,R=permanent]"]
"^", "https://%{SERVER_NAME}%{REQUEST_URI}", "[L,QSA,R=permanent]"]
"""Apache rewrite rule arguments used for redirections to https vhost"""

View file

@ -1,6 +1,9 @@
"""Module contains classes used by the Apache Configurator."""
import re
from letsencrypt.plugins import common
class Addr(common.Addr):
"""Represents an Apache address."""
def __eq__(self, other):
@ -45,39 +48,57 @@ class VirtualHost(object): # pylint: disable=too-few-public-methods
:ivar str path: Augeas path to virtual host
:ivar set addrs: Virtual Host addresses (:class:`set` of
:class:`common.Addr`)
:ivar set names: Server names/aliases of vhost
:ivar str name: ServerName of VHost
:ivar list aliases: Server aliases of vhost
(:class:`list` of :class:`str`)
:ivar bool ssl: SSLEngine on in vhost
:ivar bool enabled: Virtual host is enabled
.. todo:: Handle ServerNames appropriately...
"""
def __init__(self, filep, path, addrs, ssl, enabled, names=None):
# ?: is used for not returning enclosed characters
strip_name = re.compile(r"^(?:.+://)?([^ :$]*)")
def __init__(self, filep, path, addrs, ssl, enabled, name=None, aliases=[]):
# pylint: disable=too-many-arguments
"""Initialize a VH."""
self.filep = filep
self.path = path
self.addrs = addrs
self.names = set() if names is None else set(names)
self.name = name
self.aliases = aliases
self.ssl = ssl
self.enabled = enabled
def add_name(self, name):
def add_names(self, servername, serveralias):
"""Add name to vhost."""
self.names.add(name)
self.name = servername
self.aliases = serveralias
def get_names(self):
all_names = set(self.aliases)
# Strip out any scheme:// and <port> field from servername
if self.name is not None:
all_names.add(VirtualHost.strip_name.findall(self.name)[0])
return all_names
def __str__(self):
return (
"File: {filename}\n"
"Vhost path: {vhpath}\n"
"Addresses: {addrs}\n"
"Names: {names}\n"
"Name: {name}\n"
"Aliases: {aliases}\n"
"TLS Enabled: {tls}\n"
"Site Enabled: {active}".format(
filename=self.filep,
vhpath=self.path,
addrs=", ".join(str(addr) for addr in self.addrs),
names=", ".join(name for name in self.names),
name=self.name if self.name is not None else "",
aliases=", ".join(name for name in self.aliases),
tls="Yes" if self.ssl else "No",
active="Yes" if self.enabled else "No"))
@ -85,7 +106,74 @@ class VirtualHost(object): # pylint: disable=too-few-public-methods
if isinstance(other, self.__class__):
return (self.filep == other.filep and self.path == other.path and
self.addrs == other.addrs and
self.names == other.names and
self.get_names() == other.get_names() and
self.ssl == other.ssl and self.enabled == other.enabled)
return False
def __ne__(self, other):
return not self.__eq__(other)
def conflicts(self, addrs):
"""See if vhost conflicts with any of the addrs.
This determines whether or not these addresses would/could overwrite
the vhost addresses.
:param addrs: Iterable Addresses
:type addrs: Iterable :class:~obj.Addr
:returns: If addresses conflict with vhost
:rtype: bool
"""
# TODO: Handle domain name addrs...
for addr in addrs:
if (addr in self.addrs or addr.get_addr_obj("") in self.addrs or
addr.get_addr_obj("*") in self.addrs):
return True
return False
def same_server(self, vhost):
"""Determines if the vhost is the same 'server'.
Used in redirection - indicates whether or not the two virtual hosts
serve on the exact same IP combinations, but different ports.
.. todo:: Handle _default_
"""
if vhost.get_names() != self.get_names():
return False
# If equal and set is not empty... assume same server
if self.name is not None:
return True
# Both sets of names are empty.
# Make conservative educated guess... this is very restrictive
# Consider adding more safety checks.
if len(vhost.addrs) != len(self.addrs):
return False
# already_found acts to keep everything very conservative.
# Don't allow multiple ip:ports in same set.
already_found = set()
for addr in vhost.addrs:
for local_addr in self.addrs:
if (local_addr.get_addr() == addr.get_addr() and
local_addr != addr and
local_addr.get_addr() not in already_found):
# This intends to make sure we aren't double counting...
# e.g. 127.0.0.1:*
already_found.add(local_addr.get_addr())
break
else:
return False
return True

View file

@ -230,8 +230,7 @@ class ApacheParser(object):
Directives should be in the form of a case insensitive regex currently
.. todo:: arg should probably be a list
.. todo:: Check //* notation for including directories not intended
to be included.
.. todo:: Check //* notation for including directories
Note: Augeas is inherently case sensitive while Apache is case
insensitive. Augeas 1.0 allows case insensitive regexes like

View file

@ -185,7 +185,7 @@ class TwoVhost80Test(util.ApacheTest):
"/files" + ssl_vhost.filep + "/IfModule/VirtualHost")
self.assertEqual(len(ssl_vhost.addrs), 1)
self.assertEqual(set([obj.Addr.fromstring("*:443")]), ssl_vhost.addrs)
self.assertEqual(ssl_vhost.names, set(["encryption-example.demo"]))
self.assertEqual(ssl_vhost.name, "encryption-example.demo")
self.assertTrue(ssl_vhost.ssl)
self.assertFalse(ssl_vhost.enabled)

View file

@ -105,7 +105,7 @@ def get_vh_truth(temp_dir, config_name):
os.path.join(prefix, "encryption-example.conf"),
os.path.join(aug_pre, "encryption-example.conf/VirtualHost"),
set([obj.Addr.fromstring("*:80")]),
False, True, set(["encryption-example.demo"])),
False, True, "encryption-example.demo"),
obj.VirtualHost(
os.path.join(prefix, "default-ssl.conf"),
os.path.join(aug_pre, "default-ssl.conf/IfModule/VirtualHost"),
@ -114,12 +114,12 @@ def get_vh_truth(temp_dir, config_name):
os.path.join(prefix, "000-default.conf"),
os.path.join(aug_pre, "000-default.conf/VirtualHost"),
set([obj.Addr.fromstring("*:80")]), False, True,
set(["ip-172-30-0-17"])),
"ip-172-30-0-17"),
obj.VirtualHost(
os.path.join(prefix, "letsencrypt.conf"),
os.path.join(aug_pre, "letsencrypt.conf/VirtualHost"),
set([obj.Addr.fromstring("*:80")]), False, True,
set(["letsencrypt.demo"])),
"letsencrypt.demo"),
]
return vh_truth