Remove Apache parsing from configurator

This commit is contained in:
James Kasten
2014-12-16 21:00:14 -08:00
parent 6cd67e652b
commit 323aa350dc
6 changed files with 526 additions and 570 deletions
@@ -42,9 +42,11 @@ class Addr(object):
return self.tup[1] return self.tup[1]
def get_addr_obj(self, port): def get_addr_obj(self, port):
"""Return new address object with same addr and new port."""
return self.__class__((self.tup[0], port)) return self.__class__((self.tup[0], port))
# pylint: disable=too-few-public-methods
class VH(object): class VH(object):
"""Represents an Apache Virtualhost. """Represents an Apache Virtualhost.
+402
View File
@@ -0,0 +1,402 @@
"""ApacheParser is a member object of the ApacheConfigurator class."""
import os
import re
class ApacheParser(object):
"""Class handles the fine details of parsing the Apache Configuration."""
def __init__(self, aug, root, ssl_options):
# Find configuration root and make sure augeas can parse it.
self.aug = aug
self.root = root
self.loc = self._set_locations(ssl_options)
self._parse_file(self.loc["root"])
# Must also attempt to parse sites-available or equivalent
# Sites-available is not included naturally in configuration
self._parse_file(os.path.join(self.root, "sites-available/*"))
# This problem has been fixed in Augeas 1.0
self.standardize_excl()
def add_dir_to_ifmodssl(self, aug_conf_path, directive, val):
"""Adds directive and value to IfMod ssl block.
Adds given directive and value along configuration path within
an IfMod mod_ssl.c block. If the IfMod block does not exist in
the file, it is created.
:param str aug_conf_path: Desired Augeas config path to add directive
:param str directive: Directive you would like to add
:param str val: Value of directive ie. Listen 443, 443 is the value
"""
# TODO: Add error checking code... does the path given even exist?
# Does it throw exceptions?
if_mod_path = self._get_ifmod(aug_conf_path, "mod_ssl.c")
# IfModule can have only one valid argument, so append after
self.aug.insert(if_mod_path + "arg", "directive", False)
nvh_path = if_mod_path + "directive[1]"
self.aug.set(nvh_path, directive)
self.aug.set(nvh_path + "/arg", val)
def _get_ifmod(self, aug_conf_path, mod):
"""Returns the path to <IfMod mod> and creates one if it doesn't exist.
:param str aug_conf_path: Augeas configuration path
:param str mod: module ie. mod_ssl.c
"""
if_mods = self.aug.match(("%s/IfModule/*[self::arg='%s']" %
(aug_conf_path, mod)))
if len(if_mods) == 0:
self.aug.set("%s/IfModule[last() + 1]" % aug_conf_path, "")
self.aug.set("%s/IfModule[last()]/arg" % aug_conf_path, mod)
if_mods = self.aug.match(("%s/IfModule/*[self::arg='%s']" %
(aug_conf_path, mod)))
# Strip off "arg" at end of first ifmod path
return if_mods[0][:len(if_mods[0]) - 3]
def add_dir(self, aug_conf_path, directive, arg):
"""Appends directive to the end fo the file given by aug_conf_path.
.. note:: Not added to AugeasConfigurator because it may depend
on the lens
:param str aug_conf_path: Augeas configuration path to add directive
:param str directive: Directive to add
:param str arg: Value of the directive. ie. Listen 443, 443 is arg
"""
self.aug.set(aug_conf_path + "/directive[last() + 1]", directive)
if type(arg) is not list:
self.aug.set(aug_conf_path + "/directive[last()]/arg", arg)
else:
for i in range(len(arg)):
self.aug.set("%s/directive[last()]/arg[%d]" %
(aug_conf_path, (i+1)),
arg[i])
def find_dir(self, directive, arg=None, start=None):
"""Finds directive in the configuration.
Recursively searches through config files to find directives
Directives should be in the form of a case insensitive regex currently
.. todo:: Add order to directives returned. Last directive comes last..
.. todo:: arg should probably be a list
Note: Augeas is inherently case sensitive while Apache is case
insensitive. Augeas 1.0 allows case insensitive regexes like
regexp(/Listen/, 'i'), however the version currently supported
by Ubuntu 0.10 does not. Thus I have included my own case insensitive
transformation by calling case_i() on everything to maintain
compatibility.
:param str directive: Directive to look for
:param arg: Specific value direcitve must have, None if all should
be considered
:type arg: str or None
:param str start: Beginning Augeas path to begin looking
"""
# Cannot place member variable in the definition of the function so...
if not start:
start = get_aug_path(self.loc["root"])
# Debug code
# print "find_dir:", directive, "arg:", arg, " | Looking in:", start
# No regexp code
# if arg is None:
# matches = self.aug.match(start +
# "//*[self::directive='"+directive+"']/arg")
# else:
# matches = self.aug.match(start +
# "//*[self::directive='" + directive+"']/* [self::arg='" + arg + "']")
# includes = self.aug.match(start +
# "//* [self::directive='Include']/* [label()='arg']")
if arg is None:
matches = self.aug.match(("%s//*[self::directive=~regexp('%s')]/arg"
% (start, directive)))
else:
matches = self.aug.match(("%s//*[self::directive=~regexp('%s')]/*"
"[self::arg=~regexp('%s')]" %
(start, directive, arg)))
incl_regex = "(%s)|(%s)" % (case_i('Include'),
case_i('IncludeOptional'))
includes = self.aug.match(("%s//* [self::directive=~regexp('%s')]/* "
"[label()='arg']" % (start, incl_regex)))
# for inc in includes:
# print inc, self.aug.get(inc)
for include in includes:
# start[6:] to strip off /files
matches.extend(self.find_dir(
directive, arg, self._get_include_path(
strip_dir(start[6:]), self.aug.get(include))))
return matches
def _get_include_path(self, cur_dir, arg):
"""Converts an Apache Include directive into Augeas path.
Converts an Apache Include directive argument into an Augeas
searchable path
.. todo:: convert to use os.path.join()
:param str cur_dir: current working directory
:param str arg: Argument of Include directive
:returns: Augeas path string
:rtype: str
"""
# Sanity check argument - maybe
# Question: what can the attacker do with control over this string
# Effect parse file... maybe exploit unknown errors in Augeas
# If the attacker can Include anything though... and this function
# only operates on Apache real config data... then the attacker has
# already won.
# Perhaps it is better to simply check the permissions on all
# included files?
# check_config to validate apache config doesn't work because it
# would create a race condition between the check and this input
# TODO: Maybe... although I am convinced we have lost if
# Apache files can't be trusted. The augeas include path
# should be made to be exact.
# Check to make sure only expected characters are used <- maybe remove
# validChars = re.compile("[a-zA-Z0-9.*?_-/]*")
# matchObj = validChars.match(arg)
# if matchObj.group() != arg:
# logging.error("Error: Invalid regexp characters in %s", arg)
# return []
# Standardize the include argument based on server root
if not arg.startswith("/"):
arg = cur_dir + arg
# conf/ is a special variable for ServerRoot in Apache
elif arg.startswith("conf/"):
arg = self.root + arg[5:]
# TODO: Test if Apache allows ../ or ~/ for Includes
# Attempts to add a transform to the file if one does not already exist
self._parse_file(arg)
# Argument represents an fnmatch regular expression, convert it
# Split up the path and convert each into an Augeas accepted regex
# then reassemble
if "*" in arg or "?" in arg:
split_arg = arg.split("/")
for idx, split in enumerate(split_arg):
# * and ? are the two special fnmatch characters
if "*" in split or "?" in split:
# Turn it into a augeas regex
# TODO: Can this instead be an augeas glob instead of regex
split_arg[idx] = ("* [label()=~regexp('%s')]" %
self.fnmatch_to_re(split))
# Reassemble the argument
arg = "/".join(split_arg)
# If the include is a directory, just return the directory as a file
if arg.endswith("/"):
return get_aug_path(arg[:len(arg)-1])
return get_aug_path(arg)
def fnmatch_to_re(self, clean_fn_match): # pylint: disable=no-self-use
"""Method converts Apache's basic fnmatch to regular expression.
:param str clean_fn_match: Apache style filename match, similar to globs
:returns: regex suitable for augeas
:rtype: str
"""
regex = ""
for letter in clean_fn_match:
if letter == '.':
regex = regex + r"\."
elif letter == '*':
regex = regex + ".*"
# According to apache.org ? shouldn't appear
# but in case it is valid...
elif letter == '?':
regex = regex + "."
else:
regex = regex + letter
return regex
def _parse_file(self, file_path):
"""Parse file with Augeas
Checks to see if file_path is parsed by Augeas
If file_path isn't parsed, the file is added and Augeas is reloaded
:param str file_path: Apache config file path
"""
# Test if augeas included file for Httpd.lens
# Note: This works for augeas globs, ie. *.conf
inc_test = self.aug.match(
"/augeas/load/Httpd/incl [. ='%s']" % file_path)
if not inc_test:
# Load up files
# self.httpd_incl.append(file_path)
# self.aug.add_transform("Httpd.lns",
# self.httpd_incl, None, self.httpd_excl)
self._add_httpd_transform(file_path)
self.aug.load()
def standardize_excl(self):
"""Standardize the excl arguments for the Httpd lens in Augeas.
Note: Hack!
Standardize the excl arguments for the Httpd lens in Augeas
Servers sometimes give incorrect defaults
Note: This problem should be fixed in Augeas 1.0. Unfortunately,
Augeas 0.10 appears to be the most popular version currently.
"""
# attempt to protect against augeas error in 0.10.0 - ubuntu
# *.augsave -> /*.augsave upon augeas.load()
# Try to avoid bad httpd files
# There has to be a better way... but after a day and a half of testing
# I had no luck
# This is a hack... work around... submit to augeas if still not fixed
excl = ["*.augnew", "*.augsave", "*.dpkg-dist", "*.dpkg-bak",
"*.dpkg-new", "*.dpkg-old", "*.rpmsave", "*.rpmnew",
"*~",
self.root + "*.augsave",
self.root + "*~",
self.root + "*/*augsave",
self.root + "*/*~",
self.root + "*/*/*.augsave",
self.root + "*/*/*~"]
for i in range(len(excl)):
self.aug.set("/augeas/load/Httpd/excl[%d]" % (i+1), excl[i])
self.aug.load()
def _add_httpd_transform(self, incl):
"""Add a transform to Augeas.
This function will correctly add a transform to augeas
The existing augeas.add_transform in python is broken.
:param str incl: TODO
"""
last_include = self.aug.match("/augeas/load/Httpd/incl [last()]")
self.aug.insert(last_include[0], "incl", False)
self.aug.set("/augeas/load/Httpd/incl[last()]", incl)
def _set_locations(self, ssl_options):
"""Set default location for directives.
Locations are given as file_paths
.. todo:: Make sure that files are included
"""
root = self._find_config_root()
default = self._set_user_config_file()
temp = os.path.join(self.root, "ports.conf")
if os.path.isfile(temp):
listen = temp
name = temp
else:
listen = default
name = default
return {"root": root, "default": default, "listen": listen,
"name": name, "ssl_options": ssl_options}
def _find_config_root(self):
"""Find the Apache Configuration Root file."""
location = ["apache2.conf", "httpd.conf"]
for name in location:
if os.path.isfile(os.path.join(self.root, name)):
return os.path.join(self.root, name)
raise errors.LetsEncryptConfiguratorError(
"Could not find configuration root")
def _set_user_config_file(self, filename=''):
"""Set the appropriate user configuration file
.. todo:: This will have to be updated for other distros versions
:param str filename: optional filename that will be used as the
user config
"""
if filename:
return filename
else:
# Basic check to see if httpd.conf exists and
# in heirarchy via direct include
# httpd.conf was very common as a user file in Apache 2.2
if (os.path.isfile(self.root + 'httpd.conf') and
self.find_dir(
case_i("Include"), case_i("httpd.conf"))):
return os.path.join(self.root, 'httpd.conf')
else:
return os.path.join(self.root + 'apache2.conf')
def case_i(string):
"""Returns case insensitive regex.
Returns a sloppy, but necessary version of a case insensitive regex.
Any string should be able to be submitted and the string is
escaped and then made case insensitive.
May be replaced by a more proper /i once augeas 1.0 is widely
supported.
:param str string: string to make case i regex
"""
return "".join(["["+c.upper()+c.lower()+"]"
if c.isalpha() else c for c in re.escape(string)])
def get_aug_path(file_path):
"""Return augeas path for full filepath.
:param str file_path: Full filepath
"""
return "/files%s" % file_path
def strip_dir(path):
"""Returns directory of file path.
.. todo:: Replace this with Python standard function
:param str path: path is a file path. not an augeas section or
directive path
:returns: directory
:rtype: str
"""
index = path.rfind("/")
if index > 0:
return path[:index+1]
# No directory
return ""
+83 -532
View File
@@ -8,13 +8,14 @@ import socket
import subprocess import subprocess
import sys import sys
from letsencrypt.client import apache_obj
from letsencrypt.client import augeas_configurator from letsencrypt.client import augeas_configurator
from letsencrypt.client import challenge_util from letsencrypt.client import challenge_util
from letsencrypt.client import CONFIG from letsencrypt.client import CONFIG
from letsencrypt.client import errors from letsencrypt.client import errors
from letsencrypt.client import le_util from letsencrypt.client import le_util
from letsencrypt.client.apache import obj
from letsencrypt.client.apache import parser
# Configurator should be turned into a Singleton # Configurator should be turned into a Singleton
@@ -28,7 +29,7 @@ from letsencrypt.client import le_util
# Augeas views <VirtualHost> </Virtualhost> as an error. This will just # Augeas views <VirtualHost> </Virtualhost> as an error. This will just
# require another check_parsing_errors() after all files are included... # require another check_parsing_errors() after all files are included...
# (after a find_directive search is executed currently). It can be a one # (after a find_directive search is executed currently). It can be a one
# time check however because all of Trustifies transactions will ensure # time check however because all of LE's transactions will ensure
# only properly formed sections are added. # only properly formed sections are added.
# Note: This protocol works for filenames with spaces in it, the sites are # Note: This protocol works for filenames with spaces in it, the sites are
@@ -59,6 +60,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
parser automatically. parser automatically.
.. todo:: Add support for config file variables Define rootDir /var/www/ .. todo:: Add support for config file variables Define rootDir /var/www/
.. todo:: Add proper support for module configuration
The API of this class will change in the coming weeks as the exact The API of this class will change in the coming weeks as the exact
needs of client's are clarified with the new and developing protocol. needs of client's are clarified with the new and developing protocol.
@@ -68,7 +70,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
with the configuration with the configuration
:ivar float version: version of Apache :ivar float version: version of Apache
:ivar list vhosts: All vhosts found in the configuration :ivar list vhosts: All vhosts found in the configuration
(:class:`list` of :class:`letsencrypt.client.apache_obj.VH`) (:class:`list` of :class:`letsencrypt.client.apache.obj.VH`)
:ivar dict assoc: Mapping between domains and vhosts :ivar dict assoc: Mapping between domains and vhosts
@@ -95,8 +97,6 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
super(ApacheConfigurator, self).__init__(direc) super(ApacheConfigurator, self).__init__(direc)
self.server_root = server_root
# See if any temporary changes need to be recovered # See if any temporary changes need to be recovered
# This needs to occur before VH objects are setup... # This needs to occur before VH objects are setup...
# because this will change the underlying configuration and potential # because this will change the underlying configuration and potential
@@ -107,22 +107,13 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
if os.geteuid() == 0: if os.geteuid() == 0:
self.verify_setup() self.verify_setup()
# Find configuration root and make sure augeas can parse it. self.parser = parser.ApacheParser(self.aug, server_root, ssl_options)
self.location = self._set_locations(ssl_options) # Check for errors in parsing files with Augeas
self._parse_file(self.location["root"]) self.check_parsing_errors("httpd.aug")
# Must also attempt to parse sites-available or equivalent
# Sites-available is not included naturally in configuration
self._parse_file(os.path.join(self.server_root, "sites-available/*"))
# Set Version # Set Version
self.version = self.get_version() if version is None else version self.version = self.get_version() if version is None else version
# Check for errors in parsing files with Augeas
self.check_parsing_errors("httpd.aug")
# This problem has been fixed in Augeas 1.0
self.standardize_excl()
# Get all of the available vhosts # Get all of the available vhosts
self.vhosts = self.get_virtual_hosts() self.vhosts = self.get_virtual_hosts()
# Add name_server association dict # Add name_server association dict
@@ -158,7 +149,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
This shouldn't happen within letsencrypt though This shouldn't happen within letsencrypt though
:param vhost: ssl vhost to deploy certificate :param vhost: ssl vhost to deploy certificate
:type vhost: :class:`letsencrypt.client.apache_obj.VH` :type vhost: :class:`letsencrypt.client.apache.obj.VH`
:param str cert: certificate filename :param str cert: certificate filename
:param str key: private key filename :param str key: private key filename
@@ -170,15 +161,15 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
""" """
path = {} path = {}
path["cert_file"] = self.find_directive(case_i( path["cert_file"] = self.parser.find_dir(parser.case_i(
"SSLCertificateFile"), None, vhost.path) "SSLCertificateFile"), None, vhost.path)
path["cert_key"] = self.find_directive(case_i( path["cert_key"] = self.parser.find_dir(parser.case_i(
"SSLCertificateKeyFile"), None, vhost.path) "SSLCertificateKeyFile"), None, vhost.path)
# Only include if a certificate chain is specified # Only include if a certificate chain is specified
if cert_chain is not None: if cert_chain is not None:
path["cert_chain"] = self.find_directive( path["cert_chain"] = self.parser.find_dir(
case_i("SSLCertificateChainFile"), None, vhost.path) parser.case_i("SSLCertificateChainFile"), None, vhost.path)
if len(path["cert_file"]) == 0 or len(path["cert_key"]) == 0: if len(path["cert_file"]) == 0 or len(path["cert_key"]) == 0:
# Throw some "can't find all of the directives error" # Throw some "can't find all of the directives error"
@@ -194,7 +185,8 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
self.aug.set(path["cert_key"][0], key) self.aug.set(path["cert_key"][0], key)
if cert_chain is not None: if cert_chain is not None:
if len(path["cert_chain"]) == 0: if len(path["cert_chain"]) == 0:
self.add_dir(vhost.path, "SSLCertificateChainFile", cert_chain) self.parser.add_dir(
vhost.path, "SSLCertificateChainFile", cert_chain)
else: else:
self.aug.set(path["cert_chain"][0], cert_chain) self.aug.set(path["cert_chain"][0], cert_chain)
@@ -216,7 +208,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
:param str name: domain name :param str name: domain name
:returns: ssl vhost associated with name :returns: ssl vhost associated with name
:rtype: :class:`letsencrypt.client.apache_obj.VH` :rtype: :class:`letsencrypt.client.apache.obj.VH`
""" """
# Allows for domain names to be associated with a virtual host # Allows for domain names to be associated with a virtual host
@@ -230,7 +222,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
return vhost return vhost
# Checking for domain name in vhost address # Checking for domain name in vhost address
# This technique is not recommended by Apache but is technically valid # This technique is not recommended by Apache but is technically valid
target_addr = apache_obj.Addr((target_name, "443")) target_addr = obj.Addr((target_name, "443"))
for vhost in self.vhosts: for vhost in self.vhosts:
if target_addr in vhost.addrs: if target_addr in vhost.addrs:
return vhost return vhost
@@ -254,7 +246,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
:param str domain: domain name to associate :param str domain: domain name to associate
:param vhost: virtual host to associate with domain :param vhost: virtual host to associate with domain
:type vhost: :class:`letsencrypt.client.apache_obj.VH` :type vhost: :class:`letsencrypt.client.apache.obj.VH`
""" """
self.assoc[domain] = vhost self.assoc[domain] = vhost
@@ -287,73 +279,19 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
return all_names return all_names
def _set_locations(self, ssl_options):
"""Set default location for directives.
Locations are given as file_paths
.. todo:: Make sure that files are included
"""
root = self._find_config_root()
default = self._set_user_config_file()
temp = os.path.join(self.server_root, "ports.conf")
if os.path.isfile(temp):
listen = temp
name = temp
else:
listen = default
name = default
return {"root": root, "default": default, "listen": listen,
"name": name, "ssl_options": ssl_options}
def _find_config_root(self):
"""Find the Apache Configuration Root file."""
location = ["apache2.conf", "httpd.conf"]
for name in location:
if os.path.isfile(os.path.join(self.server_root, name)):
return os.path.join(self.server_root, name)
raise errors.LetsEncryptConfiguratorError(
"Could not find configuration root")
def _set_user_config_file(self, filename=''):
"""Set the appropriate user configuration file
.. todo:: This will have to be updated for other distros versions
:param str filename: optional filename that will be used as the
user config
"""
if filename:
return filename
else:
# Basic check to see if httpd.conf exists and
# in heirarchy via direct include
# httpd.conf was very common as a user file in Apache 2.2
if (os.path.isfile(self.server_root + 'httpd.conf') and
self.find_directive(
case_i("Include"), case_i("httpd.conf"))):
return os.path.join(self.server_root, 'httpd.conf')
else:
return os.path.join(self.server_root + 'apache2.conf')
def _add_servernames(self, host): def _add_servernames(self, host):
"""Helper function for get_virtual_hosts(). """Helper function for get_virtual_hosts().
:param host: In progress vhost whose names will be added :param host: In progress vhost whose names will be added
:type host: :class:`letsencrypt.client.apache_obj.VH` :type host: :class:`letsencrypt.client.apache.obj.VH`
""" """
name_match = self.aug.match(("%s//*[self::directive=~regexp('%s')] | " name_match = self.aug.match(("%s//*[self::directive=~regexp('%s')] | "
"%s//*[self::directive=~regexp('%s')]" % "%s//*[self::directive=~regexp('%s')]" %
(host.path, (host.path,
case_i('ServerName'), parser.case_i('ServerName'),
host.path, host.path,
case_i('ServerAlias')))) parser.case_i('ServerAlias'))))
for name in name_match: for name in name_match:
args = self.aug.match(name + "/*") args = self.aug.match(name + "/*")
@@ -366,22 +304,22 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
:param str path: Augeas path to virtual host :param str path: Augeas path to virtual host
:returns: newly created vhost :returns: newly created vhost
:rtype: :class:`letsencrypt.client.apache_obj.VH` :rtype: :class:`letsencrypt.client.apache.obj.VH`
""" """
addrs = set() addrs = set()
args = self.aug.match(path + "/arg") args = self.aug.match(path + "/arg")
for arg in args: for arg in args:
addrs.add(apache_obj.Addr.fromstring(self.aug.get(arg))) addrs.add(obj.Addr.fromstring(self.aug.get(arg)))
is_ssl = False is_ssl = False
if self.find_directive( if self.parser.find_dir(
case_i("SSLEngine"), case_i("on"), path): parser.case_i("SSLEngine"), parser.case_i("on"), path):
is_ssl = True is_ssl = True
filename = get_file_path(path) filename = get_file_path(path)
is_enabled = self.is_site_enabled(filename) is_enabled = self.is_site_enabled(filename)
vhost = apache_obj.VH(filename, path, addrs, is_ssl, is_enabled) vhost = obj.VH(filename, path, addrs, is_ssl, is_enabled)
self._add_servernames(vhost) self._add_servernames(vhost)
return vhost return vhost
@@ -389,7 +327,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
def get_virtual_hosts(self): def get_virtual_hosts(self):
"""Returns list of virtual hosts found in the Apache configuration. """Returns list of virtual hosts found in the Apache configuration.
:returns: List of :class:`letsencrypt.client.apache_obj.VH` objects :returns: List of :class:`letsencrypt.client.apache.obj.VH` objects
found in configuration found in configuration
:rtype: list :rtype: list
@@ -397,7 +335,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
# Search sites-available, httpd.conf for possible virtual hosts # Search sites-available, httpd.conf for possible virtual hosts
paths = self.aug.match( paths = self.aug.match(
("/files%ssites-available//*[label()=~regexp('%s')]" % ("/files%ssites-available//*[label()=~regexp('%s')]" %
(self.server_root, case_i('VirtualHost')))) (self.parser.root, parser.case_i('VirtualHost'))))
vhs = [] vhs = []
for path in paths: for path in paths:
@@ -425,8 +363,9 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
# search for NameVirtualHost directive for ip_addr # search for NameVirtualHost directive for ip_addr
# note ip_addr can be FQDN although Apache does not recommend it # note ip_addr can be FQDN although Apache does not recommend it
return (self.version >= (2, 4) or return (self.version >= (2, 4) or
self.find_directive( self.parser.find_dir(
case_i("NameVirtualHost"), case_i(str(target_addr)))) parser.case_i("NameVirtualHost"),
parser.case_i(str(target_addr))))
def add_name_vhost(self, addr): def add_name_vhost(self, addr):
"""Adds NameVirtualHost directive for given address. """Adds NameVirtualHost directive for given address.
@@ -434,33 +373,13 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
:param str addr: Address that will be added as NameVirtualHost directive :param str addr: Address that will be added as NameVirtualHost directive
""" """
path = self._add_dir_to_ifmodssl( path = self.parser.add_dir_to_ifmodssl(
get_aug_path(self.location["name"]), "NameVirtualHost", str(addr)) parser.get_aug_path(
self.parser.loc["name"]), "NameVirtualHost", str(addr))
self.save_notes += "Setting %s to be NameBasedVirtualHost\n" % addr self.save_notes += "Setting %s to be NameBasedVirtualHost\n" % addr
self.save_notes += "\tDirective added to %s\n" % path self.save_notes += "\tDirective added to %s\n" % path
def _add_dir_to_ifmodssl(self, aug_conf_path, directive, val):
"""Adds directive and value to IfMod ssl block.
Adds given directive and value along configuration path within
an IfMod mod_ssl.c block. If the IfMod block does not exist in
the file, it is created.
:param str aug_conf_path: Desired Augeas config path to add directive
:param str directive: Directive you would like to add
:param str val: Value of directive ie. Listen 443, 443 is the value
"""
# TODO: Add error checking code... does the path given even exist?
# Does it throw exceptions?
if_mod_path = self._get_ifmod(aug_conf_path, "mod_ssl.c")
# IfModule can have only one valid argument, so append after
self.aug.insert(if_mod_path + "arg", "directive", False)
nvh_path = if_mod_path + "directive[1]"
self.aug.set(nvh_path, directive)
self.aug.set(nvh_path + "/arg", val)
def _prepare_server_https(self): def _prepare_server_https(self):
"""Prepare the server for HTTPS. """Prepare the server for HTTPS.
@@ -475,18 +394,18 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
# Check for Listen 443 # Check for Listen 443
# Note: This could be made to also look for ip:443 combo # Note: This could be made to also look for ip:443 combo
# TODO: Need to search only open directives and IfMod mod_ssl.c # TODO: Need to search only open directives and IfMod mod_ssl.c
if len(self.find_directive(case_i("Listen"), "443")) == 0: if len(self.parser.find_dir(parser.case_i("Listen"), "443")) == 0:
logging.debug("No Listen 443 directive found") logging.debug("No Listen 443 directive found")
logging.debug("Setting the Apache Server to Listen on port 443") logging.debug("Setting the Apache Server to Listen on port 443")
path = self._add_dir_to_ifmodssl( path = self.parser.add_dir_to_ifmodssl(
get_aug_path(self.location["listen"]), "Listen", "443") parser.get_aug_path(self.parser.loc["listen"]), "Listen", "443")
self.save_notes += "Added Listen 443 directive to %s\n" % path self.save_notes += "Added Listen 443 directive to %s\n" % path
def make_server_sni_ready(self, vhost, default_addr="*:443"): def make_server_sni_ready(self, vhost, default_addr="*:443"):
"""Checks to see if the server is ready for SNI challenges. """Checks to see if the server is ready for SNI challenges.
:param vhost: VHost to check SNI compatibility :param vhost: VHost to check SNI compatibility
:type vhost: :class:`letsencrypt.client.apache_obj.VH` :type vhost: :class:`letsencrypt.client.apache.obj.VH`
:param str default_addr: TODO - investigate function further :param str default_addr: TODO - investigate function further
@@ -509,178 +428,6 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
"based virtual host", addr) "based virtual host", addr)
self.add_name_vhost(addr) self.add_name_vhost(addr)
def _get_ifmod(self, aug_conf_path, mod):
"""Returns the path to <IfMod mod> and creates one if it doesn't exist.
:param str aug_conf_path: Augeas configuration path
:param str mod: module ie. mod_ssl.c
"""
if_mods = self.aug.match(("%s/IfModule/*[self::arg='%s']" %
(aug_conf_path, mod)))
if len(if_mods) == 0:
self.aug.set("%s/IfModule[last() + 1]" % aug_conf_path, "")
self.aug.set("%s/IfModule[last()]/arg" % aug_conf_path, mod)
if_mods = self.aug.match(("%s/IfModule/*[self::arg='%s']" %
(aug_conf_path, mod)))
# Strip off "arg" at end of first ifmod path
return if_mods[0][:len(if_mods[0]) - 3]
def add_dir(self, aug_conf_path, directive, arg):
"""Appends directive to the end fo the file given by aug_conf_path.
.. note:: Not added to AugeasConfigurator because it may depend
on the lens
:param str aug_conf_path: Augeas configuration path to add directive
:param str directive: Directive to add
:param str arg: Value of the directive. ie. Listen 443, 443 is arg
"""
self.aug.set(aug_conf_path + "/directive[last() + 1]", directive)
if type(arg) is not list:
self.aug.set(aug_conf_path + "/directive[last()]/arg", arg)
else:
for i in range(len(arg)):
self.aug.set("%s/directive[last()]/arg[%d]" %
(aug_conf_path, (i+1)),
arg[i])
def find_directive(self, directive, arg=None, start=None):
"""Finds directive in the configuration.
Recursively searches through config files to find directives
Directives should be in the form of a case insensitive regex currently
.. todo:: arg should probably be a list
Note: Augeas is inherently case sensitive while Apache is case
insensitive. Augeas 1.0 allows case insensitive regexes like
regexp(/Listen/, 'i'), however the version currently supported
by Ubuntu 0.10 does not. Thus I have included my own case insensitive
transformation by calling case_i() on everything to maintain
compatibility.
:param str directive: Directive to look for
:param arg: Specific value direcitve must have, None if all should
be considered
:type arg: str or None
:param str start: Beginning Augeas path to begin looking
"""
# Cannot place member variable in the definition of the function so...
if not start:
start = get_aug_path(self.location["root"])
# Debug code
# print "find_dir:", directive, "arg:", arg, " | Looking in:", start
# No regexp code
# if arg is None:
# matches = self.aug.match(start +
# "//*[self::directive='"+directive+"']/arg")
# else:
# matches = self.aug.match(start +
# "//*[self::directive='" + directive+"']/* [self::arg='" + arg + "']")
# includes = self.aug.match(start +
# "//* [self::directive='Include']/* [label()='arg']")
if arg is None:
matches = self.aug.match(("%s//*[self::directive=~regexp('%s')]/arg"
% (start, directive)))
else:
matches = self.aug.match(("%s//*[self::directive=~regexp('%s')]/*"
"[self::arg=~regexp('%s')]" %
(start, directive, arg)))
incl_regex = "(%s)|(%s)" % (case_i('Include'),
case_i('IncludeOptional'))
includes = self.aug.match(("%s//* [self::directive=~regexp('%s')]/* "
"[label()='arg']" % (start, incl_regex)))
# for inc in includes:
# print inc, self.aug.get(inc)
for include in includes:
# start[6:] to strip off /files
matches.extend(self.find_directive(
directive, arg, self._get_include_path(strip_dir(start[6:]),
self.aug.get(include))))
return matches
def _get_include_path(self, cur_dir, arg):
"""Converts an Apache Include directive into Augeas path.
Converts an Apache Include directive argument into an Augeas
searchable path
.. todo:: convert to use os.path.join()
:param str cur_dir: current working directory
:param str arg: Argument of Include directive
:returns: Augeas path string
:rtype: str
"""
# Sanity check argument - maybe
# Question: what can the attacker do with control over this string
# Effect parse file... maybe exploit unknown errors in Augeas
# If the attacker can Include anything though... and this function
# only operates on Apache real config data... then the attacker has
# already won.
# Perhaps it is better to simply check the permissions on all
# included files?
# check_config to validate apache config doesn't work because it
# would create a race condition between the check and this input
# TODO: Maybe... although I am convinced we have lost if
# Apache files can't be trusted. The augeas include path
# should be made to be exact.
# Check to make sure only expected characters are used <- maybe remove
# validChars = re.compile("[a-zA-Z0-9.*?_-/]*")
# matchObj = validChars.match(arg)
# if matchObj.group() != arg:
# logging.error("Error: Invalid regexp characters in %s", arg)
# return []
# Standardize the include argument based on server root
if not arg.startswith("/"):
arg = cur_dir + arg
# conf/ is a special variable for ServerRoot in Apache
elif arg.startswith("conf/"):
arg = self.server_root + arg[5:]
# TODO: Test if Apache allows ../ or ~/ for Includes
# Attempts to add a transform to the file if one does not already exist
self._parse_file(arg)
# Argument represents an fnmatch regular expression, convert it
# Split up the path and convert each into an Augeas accepted regex
# then reassemble
if "*" in arg or "?" in arg:
split_arg = arg.split("/")
for idx, split in enumerate(split_arg):
# * and ? are the two special fnmatch characters
if "*" in split or "?" in split:
# Turn it into a augeas regex
# TODO: Can this instead be an augeas glob instead of regex
split_arg[idx] = ("* [label()=~regexp('%s')]" %
self.fnmatch_to_re(split))
# Reassemble the argument
arg = "/".join(split_arg)
# If the include is a directory, just return the directory as a file
if arg.endswith("/"):
return get_aug_path(arg[:len(arg)-1])
return get_aug_path(arg)
def make_vhost_ssl(self, nonssl_vhost): def make_vhost_ssl(self, nonssl_vhost):
"""Makes an ssl_vhost version of a nonssl_vhost. """Makes an ssl_vhost version of a nonssl_vhost.
@@ -688,10 +435,10 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
New vhost will reside as (nonssl_vhost.path) + CONFIG.LE_VHOST_EXT New vhost will reside as (nonssl_vhost.path) + CONFIG.LE_VHOST_EXT
:param nonssl_vhost: Valid VH that doesn't have SSLEngine on :param nonssl_vhost: Valid VH that doesn't have SSLEngine on
:type nonssl_vhost: :class:`letsencrypt.client.apache_obj.VH` :type nonssl_vhost: :class:`letsencrypt.client.apache.obj.VH`
:returns: SSL vhost :returns: SSL vhost
:rtype: :class:`letsencrypt.client.apache_obj.VH` :rtype: :class:`letsencrypt.client.apache.obj.VH`
""" """
avail_fp = nonssl_vhost.filep avail_fp = nonssl_vhost.filep
@@ -726,10 +473,10 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
# change address to address:443 # change address to address:443
addr_match = "/files%s//* [label()=~regexp('%s')]/arg" addr_match = "/files%s//* [label()=~regexp('%s')]/arg"
ssl_addr_p = self.aug.match( ssl_addr_p = self.aug.match(
addr_match % (ssl_fp, case_i('VirtualHost'))) addr_match % (ssl_fp, parser.case_i('VirtualHost')))
for i in range(len(ssl_addr_p)): for i in range(len(ssl_addr_p)):
ssl_addr_arg = apache_obj.Addr.fromstring( ssl_addr_arg = obj.Addr.fromstring(
str(self.aug.get(ssl_addr_p[i]))) str(self.aug.get(ssl_addr_p[i])))
ssl_addr_arg.set_port("443") ssl_addr_arg.set_port("443")
self.aug.set(ssl_addr_p[i], str(ssl_addr_arg)) self.aug.set(ssl_addr_p[i], str(ssl_addr_arg))
@@ -737,16 +484,16 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
# Add directives # Add directives
vh_p = self.aug.match(("/files%s//* [label()=~regexp('%s')]" % vh_p = self.aug.match(("/files%s//* [label()=~regexp('%s')]" %
(ssl_fp, case_i('VirtualHost')))) (ssl_fp, parser.case_i('VirtualHost'))))
if len(vh_p) != 1: if len(vh_p) != 1:
logging.error("Error: should only be one vhost in %s", avail_fp) logging.error("Error: should only be one vhost in %s", avail_fp)
sys.exit(1) sys.exit(1)
self.add_dir(vh_p[0], "SSLCertificateFile", self.parser.add_dir(vh_p[0], "SSLCertificateFile",
"/etc/ssl/certs/ssl-cert-snakeoil.pem") "/etc/ssl/certs/ssl-cert-snakeoil.pem")
self.add_dir(vh_p[0], "SSLCertificateKeyFile", self.parser.add_dir(vh_p[0], "SSLCertificateKeyFile",
"/etc/ssl/private/ssl-cert-snakeoil.key") "/etc/ssl/private/ssl-cert-snakeoil.key")
self.add_dir(vh_p[0], "Include", self.location["ssl_options"]) self.parser.add_dir(vh_p[0], "Include", self.parser.loc["ssl_options"])
# Log actions and create save notes # Log actions and create save notes
logging.info("Created an SSL vhost at %s", ssl_fp) logging.info("Created an SSL vhost at %s", ssl_fp)
@@ -785,10 +532,10 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
The function then adds the directive The function then adds the directive
:param ssl_vhost: Destination of traffic, an ssl enabled vhost :param ssl_vhost: Destination of traffic, an ssl enabled vhost
:type ssl_vhost: :class:`letsencrypt.client.apache_obj.VH` :type ssl_vhost: :class:`letsencrypt.client.apache.obj.VH`
:returns: Success, general_vhost (HTTP vhost) :returns: Success, general_vhost (HTTP vhost)
:rtype: (bool, :class:`letsencrypt.client.apache_obj.VH`) :rtype: (bool, :class:`letsencrypt.client.apache.obj.VH`)
""" """
# TODO: Enable check to see if it is already there # TODO: Enable check to see if it is already there
@@ -812,9 +559,9 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
logging.debug("Unknown redirect exists for this vhost") logging.debug("Unknown redirect exists for this vhost")
return False, general_v return False, general_v
# Add directives to server # Add directives to server
self.add_dir(general_v.path, "RewriteEngine", "On") self.parser.add_dir(general_v.path, "RewriteEngine", "On")
self.add_dir(general_v.path, self.parser.add_dir(
"RewriteRule", CONFIG.REWRITE_HTTPS_ARGS) general_v.path, "RewriteRule", CONFIG.REWRITE_HTTPS_ARGS)
self.save_notes += ('Redirecting host in %s to ssl vhost in %s\n' % self.save_notes += ('Redirecting host in %s to ssl vhost in %s\n' %
(general_v.filep, ssl_vhost.filep)) (general_v.filep, ssl_vhost.filep))
self.save() self.save()
@@ -834,16 +581,16 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
-1 is also returned in case of no redirection/rewrite directives -1 is also returned in case of no redirection/rewrite directives
:param vhost: vhost to check :param vhost: vhost to check
:type vhost: :class:`letsencrypt.client.apache_obj.VH` :type vhost: :class:`letsencrypt.client.apache.obj.VH`
:returns: Success, code value... see documentation :returns: Success, code value... see documentation
:rtype: bool, int :rtype: bool, int
""" """
rewrite_path = self.find_directive( rewrite_path = self.parser.find_dir(
case_i("RewriteRule"), None, vhost.path) parser.case_i("RewriteRule"), None, vhost.path)
redirect_path = self.find_directive( redirect_path = self.parser.find_dir(
case_i("Redirect"), None, vhost.path) parser.case_i("Redirect"), None, vhost.path)
if redirect_path: if redirect_path:
# "Existing Redirect directive for virtualhost" # "Existing Redirect directive for virtualhost"
@@ -865,10 +612,10 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
"""Creates an http_vhost specifically to redirect for the ssl_vhost. """Creates an http_vhost specifically to redirect for the ssl_vhost.
:param ssl_vhost: ssl vhost :param ssl_vhost: ssl vhost
:type ssl_vhost: :class:`letsencrypt.client.apache_obj.VH` :type ssl_vhost: :class:`letsencrypt.client.apache.obj.VH`
:returns: Success, vhost :returns: Success, vhost
:rtype: (bool, :class:`letsencrypt.client.apache_obj.VH`) :rtype: (bool, :class:`letsencrypt.client.apache.obj.VH`)
""" """
# Consider changing this to a dictionary check # Consider changing this to a dictionary check
@@ -914,7 +661,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
redirect_filename = "le-redirect-%s.conf" % ssl_vhost.names[0] redirect_filename = "le-redirect-%s.conf" % ssl_vhost.names[0]
redirect_filepath = ("%ssites-available/%s" % redirect_filepath = ("%ssites-available/%s" %
(self.server_root, redirect_filename)) (self.parser.root, redirect_filename))
# Register the new file that will be created # Register the new file that will be created
# Note: always register the creation before writing to ensure file will # Note: always register the creation before writing to ensure file will
@@ -928,8 +675,8 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
self.aug.load() self.aug.load()
# Make a new vhost data structure and add it to the lists # Make a new vhost data structure and add it to the lists
new_fp = self.server_root + "sites-available/" + redirect_filename new_fp = self.parser.root + "sites-available/" + redirect_filename
new_vhost = self._create_vhost(get_aug_path(new_fp)) new_vhost = self._create_vhost(parser.get_aug_path(new_fp))
self.vhosts.append(new_vhost) self.vhosts.append(new_vhost)
# Finally create documentation for the change # Finally create documentation for the change
@@ -951,7 +698,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
if not conflict: returns space separated list of new host addrs if not conflict: returns space separated list of new host addrs
:param ssl_vhost: SSL Vhost to check for possible port 80 redirection :param ssl_vhost: SSL Vhost to check for possible port 80 redirection
:type ssl_vhost: :class:`letsencrypt.client.apache_obj.VH` :type ssl_vhost: :class:`letsencrypt.client.apache.obj.VH`
:returns: TODO :returns: TODO
:rtype: TODO :rtype: TODO
@@ -984,18 +731,18 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
Consider changing this into a dict check Consider changing this into a dict check
:param ssl_vhost: ssl vhost to check :param ssl_vhost: ssl vhost to check
:type ssl_vhost: :class:`letsencrypt.client.apache_obj.VH` :type ssl_vhost: :class:`letsencrypt.client.apache.obj.VH`
:returns: HTTP vhost or None if unsuccessful :returns: HTTP vhost or None if unsuccessful
:rtype: :class:`letsencrypt.client.apache_obj.VH` or None :rtype: :class:`letsencrypt.client.apache.obj.VH` or None
""" """
# _default_:443 check # _default_:443 check
# Instead... should look for vhost of the form *:80 # Instead... should look for vhost of the form *:80
# Should we prompt the user? # Should we prompt the user?
ssl_addrs = ssl_vhost.addrs ssl_addrs = ssl_vhost.addrs
if ssl_addrs == apache_obj.Addr.fromstring("_default_:443"): if ssl_addrs == obj.Addr.fromstring("_default_:443"):
ssl_addrs = [apache_obj.Addr.fromstring("*:443")] ssl_addrs = [obj.Addr.fromstring("*:443")]
for vhost in self.vhosts: for vhost in self.vhosts:
found = 0 found = 0
@@ -1038,10 +785,10 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
for vhost in self.vhosts: for vhost in self.vhosts:
if vhost.ssl: if vhost.ssl:
cert_path = self.find_directive( cert_path = self.parser.find_dir(
case_i("SSLCertificateFile"), None, vhost.path) parser.case_i("SSLCertificateFile"), None, vhost.path)
key_path = self.find_directive( key_path = self.parser.find_dir(
case_i("SSLCertificateKeyFile"), None, vhost.path) parser.case_i("SSLCertificateKeyFile"), None, vhost.path)
# Can be removed once find directive can return ordered results # Can be removed once find directive can return ordered results
if len(cert_path) != 1 or len(key_path) != 1: if len(cert_path) != 1 or len(key_path) != 1:
@@ -1066,7 +813,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
:rtype: bool :rtype: bool
""" """
enabled_dir = os.path.join(self.server_root, "sites-enabled/") enabled_dir = os.path.join(self.parser.root, "sites-enabled/")
for entry in os.listdir(enabled_dir): for entry in os.listdir(enabled_dir):
if os.path.realpath(enabled_dir + entry) == avail_fp: if os.path.realpath(enabled_dir + entry) == avail_fp:
return True return True
@@ -1081,7 +828,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
.. todo:: Make sure link is not broken... .. todo:: Make sure link is not broken...
:param vhost: vhost to enable :param vhost: vhost to enable
:type vhost: :class:`letsencrypt.client.apache_obj.VH` :type vhost: :class:`letsencrypt.client.apache.obj.VH`
:returns: Success :returns: Success
:rtype: bool :rtype: bool
@@ -1092,7 +839,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
if "/sites-available/" in vhost.filep: if "/sites-available/" in vhost.filep:
enabled_path = ("%ssites-enabled/%s" % enabled_path = ("%ssites-enabled/%s" %
(self.server_root, os.path.basename(vhost.filep))) (self.parser.root, os.path.basename(vhost.filep)))
self.register_file_creation(False, enabled_path) self.register_file_creation(False, enabled_path)
os.symlink(vhost.filep, enabled_path) os.symlink(vhost.filep, enabled_path)
vhost.enabled = True vhost.enabled = True
@@ -1101,82 +848,6 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
return True return True
return False return False
def fnmatch_to_re(self, clean_fn_match): # pylint: disable=no-self-use
"""Method converts Apache's basic fnmatch to regular expression.
:param str clean_fn_match: Apache style filename match, similar to globs
:returns: regex suitable for augeas
:rtype: str
"""
regex = ""
for letter in clean_fn_match:
if letter == '.':
regex = regex + r"\."
elif letter == '*':
regex = regex + ".*"
# According to apache.org ? shouldn't appear
# but in case it is valid...
elif letter == '?':
regex = regex + "."
else:
regex = regex + letter
return regex
def _parse_file(self, file_path):
"""Parse file with Augeas
Checks to see if file_path is parsed by Augeas
If file_path isn't parsed, the file is added and Augeas is reloaded
:param str file_path: Apache config file path
"""
# Test if augeas included file for Httpd.lens
# Note: This works for augeas globs, ie. *.conf
inc_test = self.aug.match(
"/augeas/load/Httpd/incl [. ='%s']" % file_path)
if not inc_test:
# Load up files
# self.httpd_incl.append(file_path)
# self.aug.add_transform("Httpd.lns",
# self.httpd_incl, None, self.httpd_excl)
self._add_httpd_transform(file_path)
self.aug.load()
def standardize_excl(self):
"""Standardize the excl arguments for the Httpd lens in Augeas.
Note: Hack!
Standardize the excl arguments for the Httpd lens in Augeas
Servers sometimes give incorrect defaults
Note: This problem should be fixed in Augeas 1.0. Unfortunately,
Augeas 0.10 appears to be the most popular version currently.
"""
# attempt to protect against augeas error in 0.10.0 - ubuntu
# *.augsave -> /*.augsave upon augeas.load()
# Try to avoid bad httpd files
# There has to be a better way... but after a day and a half of testing
# I had no luck
# This is a hack... work around... submit to augeas if still not fixed
excl = ["*.augnew", "*.augsave", "*.dpkg-dist", "*.dpkg-bak",
"*.dpkg-new", "*.dpkg-old", "*.rpmsave", "*.rpmnew",
"*~",
self.server_root + "*.augsave",
self.server_root + "*~",
self.server_root + "*/*augsave",
self.server_root + "*/*~",
self.server_root + "*/*/*.augsave",
self.server_root + "*/*/*~"]
for i in range(len(excl)):
self.aug.set("/augeas/load/Httpd/excl[%d]" % (i+1), excl[i])
self.aug.load()
def restart(self, quiet=False): # pylint: disable=no-self-use def restart(self, quiet=False): # pylint: disable=no-self-use
"""Restarts apache server. """Restarts apache server.
@@ -1186,19 +857,6 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
""" """
return apache_restart() return apache_restart()
def _add_httpd_transform(self, incl):
"""Add a transform to Augeas.
This function will correctly add a transform to augeas
The existing augeas.add_transform in python is broken.
:param str incl: TODO
"""
last_include = self.aug.match("/augeas/load/Httpd/incl [last()]")
self.aug.insert(last_include[0], "incl", False)
self.aug.set("/augeas/load/Httpd/incl[last()]", incl)
def config_test(self): def config_test(self):
"""Check the configuration of Apache for errors. """Check the configuration of Apache for errors.
@@ -1376,7 +1034,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
:type dvsni_key: :class:`letsencrypt.client.client.Client.Key` :type dvsni_key: :class:`letsencrypt.client.client.Client.Key`
:param list ll_addrs: list of list of :param list ll_addrs: list of list of
:class:`letsencrypt.client.apache_obj.Addr` to apply :class:`letsencrypt.client.apache.obj.Addr` to apply
""" """
# WARNING: THIS IS A POTENTIAL SECURITY VULNERABILITY # WARNING: THIS IS A POTENTIAL SECURITY VULNERABILITY
@@ -1398,7 +1056,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
list_sni_tuple[idx][2], lis, dvsni_key.file) list_sni_tuple[idx][2], lis, dvsni_key.file)
config_text += "</IfModule> \n" config_text += "</IfModule> \n"
self.dvsni_conf_include_check(self.location["default"]) self.dvsni_conf_include_check(self.parser.loc["default"])
self.register_file_creation(True, CONFIG.APACHE_CHALLENGE_CONF) self.register_file_creation(True, CONFIG.APACHE_CHALLENGE_CONF)
with open(CONFIG.APACHE_CHALLENGE_CONF, 'w') as new_conf: with open(CONFIG.APACHE_CHALLENGE_CONF, 'w') as new_conf:
@@ -1413,18 +1071,18 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
:param str main_config: file path to main user apache config file :param str main_config: file path to main user apache config file
""" """
if len(self.find_directive( if len(self.parser.find_dir(
case_i("Include"), CONFIG.APACHE_CHALLENGE_CONF)) == 0: parser.case_i("Include"), CONFIG.APACHE_CHALLENGE_CONF)) == 0:
# print "Including challenge virtual host(s)" # print "Including challenge virtual host(s)"
self.add_dir(get_aug_path(main_config), self.parser.add_dir(parser.get_aug_path(main_config),
"Include", CONFIG.APACHE_CHALLENGE_CONF) "Include", CONFIG.APACHE_CHALLENGE_CONF)
def get_config_text(self, nonce, ip_addrs, dvsni_key_file): def get_config_text(self, nonce, ip_addrs, dvsni_key_file):
"""Chocolate virtual server configuration text """Chocolate virtual server configuration text
:param str nonce: hex form of nonce :param str nonce: hex form of nonce
:param list ip_addrs: addresses of challenged domain :param list ip_addrs: addresses of challenged domain
:class:`list` of type :class:`letsencrypt.client.apache_obj.Addr` :class:`list` of type :class:`letsencrypt.client.apache.obj.Addr`
:param str dvsni_key_file: Path to key file :param str dvsni_key_file: Path to key file
:returns: virtual host configuration text :returns: virtual host configuration text
@@ -1439,7 +1097,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
"\n" "\n"
"LimitRequestBody 1048576\n" "LimitRequestBody 1048576\n"
"\n" "\n"
"Include " + self.location["ssl_options"] + "\n" "Include " + self.parser.loc["ssl_options"] + "\n"
"SSLCertificateFile " + self.dvsni_get_cert_file(nonce) + "\n" "SSLCertificateFile " + self.dvsni_get_cert_file(nonce) + "\n"
"SSLCertificateKeyFile " + dvsni_key_file + "\n" "SSLCertificateKeyFile " + dvsni_key_file + "\n"
"\n" "\n"
@@ -1538,22 +1196,6 @@ def apache_restart():
return True return True
def case_i(string):
"""Returns case insensitive regex.
Returns a sloppy, but necessary version of a case insensitive regex.
Any string should be able to be submitted and the string is
escaped and then made case insensitive.
May be replaced by a more proper /i once augeas 1.0 is widely
supported.
:param str string: string to make case i regex
"""
return "".join(["["+c.upper()+c.lower()+"]"
if c.isalpha() else c for c in re.escape(string)])
def get_file_path(vhost_path): def get_file_path(vhost_path):
"""Get file path from augeas_vhost_path. """Get file path from augeas_vhost_path.
@@ -1580,94 +1222,3 @@ def get_file_path(vhost_path):
continue continue
break break
return avail_fp return avail_fp
def get_aug_path(file_path):
"""Return augeas path for full filepath.
:param str file_path: Full filepath
"""
return "/files%s" % file_path
def strip_dir(path):
"""Returns directory of file path.
.. todo:: Replace this with Python standard function
:param str path: path is a file path. not an augeas section or
directive path
:returns: directory
:rtype: str
"""
index = path.rfind("/")
if index > 0:
return path[:index+1]
# No directory
return ""
def main():
"""Main function used for quick testing purposes"""
config = ApacheConfigurator()
# for v in config.vhosts:
# print v.filep
# print v.addrs
# for name in v.names:
# print name
print config.find_directive(
case_i("NameVirtualHost"), case_i("holla:443"))
# for m in config.find_directive("Listen", "443"):
# print "Directive Path:", m, "Value:", config.aug.get(m)
# for v in config.vhosts:
# for a in v.addrs:
# print "Address:",a, "- Is name vhost?", config.is_name_vhost(a)
# print config.get_all_names()
# test_file = "/home/james/Desktop/ports_test.conf"
# config._parse_file(test_file)
# config.aug.insert("/files"+test_file+"/IfModule[1]/arg","directive",False)
# config.aug.set("/files"+test_file+"/IfModule[1]/directive[1]", "Listen")
# config.aug.set(
# "/files" +test_file+ "/IfModule[1]/directive[1]/arg", "556")
# #config.save_notes = "Added listen 431 for test"
# #config.register_file_creation("/home/james/Desktop/new_file.txt")
# #config.save("Testing Saves", False)
# #config.recover_checkpoint(1)
# # config.display_checkpoints()
config.config_test()
# # Testing redirection and make_vhost_ssl
# ssl_vh = None
# for vh in config.vhosts:
# if not vh.addrs:
# print vh.names
# print vh.filep
# if vh.addrs[0] == "23.20.47.131:80":
# print "Here we go"
# ssl_vh = config.make_vhost_ssl(vh)
# config.enable_redirect(ssl_vh)
# for vh in config.vhosts:
# if len(vh.names) > 0:
# config.deploy_cert(
# vh,
# "/home/james/Documents/apache_choc/req.pem",
# "/home/james/Documents/apache_choc/key.pem",
# "/home/james/Downloads/sub.class1.server.ca.pem")
if __name__ == "__main__":
main()
@@ -15,8 +15,6 @@ from letsencrypt.client import le_util
class AugeasConfigurator(configurator.Configurator): class AugeasConfigurator(configurator.Configurator):
"""Base Augeas Configurator class. """Base Augeas Configurator class.
.. todo:: Fix generic exception handling.
:ivar aug: Augeas object :ivar aug: Augeas object
:type aug: :class:`augeas.Augeas` :type aug: :class:`augeas.Augeas`
@@ -8,12 +8,12 @@ import unittest
import mock import mock
from letsencrypt.client import apache_obj
from letsencrypt.client import apache_configurator from letsencrypt.client import apache_configurator
from letsencrypt.client import CONFIG from letsencrypt.client import CONFIG
from letsencrypt.client import display from letsencrypt.client import display
from letsencrypt.client import errors from letsencrypt.client import errors
from letsencrypt.client.apache import obj
from letsencrypt.client.apache import parser
UBUNTU_CONFIGS = pkg_resources.resource_filename( UBUNTU_CONFIGS = pkg_resources.resource_filename(
__name__, "testdata/debian_apache_2_4") __name__, "testdata/debian_apache_2_4")
@@ -62,24 +62,24 @@ class TwoVhost80Test(unittest.TestCase):
self.temp_dir, "two_vhost_80/apache2/sites-available") self.temp_dir, "two_vhost_80/apache2/sites-available")
aug_pre = "/files" + prefix aug_pre = "/files" + prefix
self.vh_truth = [ self.vh_truth = [
apache_obj.VH( obj.VH(
os.path.join(prefix, "encryption-example.conf"), os.path.join(prefix, "encryption-example.conf"),
os.path.join(aug_pre, "encryption-example.conf/VirtualHost"), os.path.join(aug_pre, "encryption-example.conf/VirtualHost"),
set([apache_obj.Addr.fromstring("*:80")]), set([obj.Addr.fromstring("*:80")]),
False, True, set(["encryption-example.demo"])), False, True, set(["encryption-example.demo"])),
apache_obj.VH( obj.VH(
os.path.join(prefix, "default-ssl.conf"), os.path.join(prefix, "default-ssl.conf"),
os.path.join(aug_pre, "default-ssl.conf/IfModule/VirtualHost"), os.path.join(aug_pre, "default-ssl.conf/IfModule/VirtualHost"),
set([apache_obj.Addr.fromstring("_default_:443")]), True, False), set([obj.Addr.fromstring("_default_:443")]), True, False),
apache_obj.VH( obj.VH(
os.path.join(prefix, "000-default.conf"), os.path.join(prefix, "000-default.conf"),
os.path.join(aug_pre, "000-default.conf/VirtualHost"), os.path.join(aug_pre, "000-default.conf/VirtualHost"),
set([apache_obj.Addr.fromstring("*:80")]), False, True, set([obj.Addr.fromstring("*:80")]), False, True,
set(["ip-172-30-0-17"])), set(["ip-172-30-0-17"])),
apache_obj.VH( obj.VH(
os.path.join(prefix, "letsencrypt.conf"), os.path.join(prefix, "letsencrypt.conf"),
os.path.join(aug_pre, "letsencrypt.conf/VirtualHost"), os.path.join(aug_pre, "letsencrypt.conf/VirtualHost"),
set([apache_obj.Addr.fromstring("*:80")]), False, True, set([obj.Addr.fromstring("*:80")]), False, True,
set(["letsencrypt.demo"])), set(["letsencrypt.demo"])),
] ]
@@ -97,7 +97,9 @@ class TwoVhost80Test(unittest.TestCase):
""" """
file_path = os.path.join( file_path = os.path.join(
self.config_path, "sites-available", "letsencrypt.conf") self.config_path, "sites-available", "letsencrypt.conf")
self.config._parse_file(file_path) # pylint: disable=protected-access
# pylint: disable=protected-access
self.config.parser._parse_file(file_path)
# search for the httpd incl # search for the httpd incl
matches = self.config.aug.match( matches = self.config.aug.match(
@@ -110,12 +112,12 @@ class TwoVhost80Test(unittest.TestCase):
self.assertEqual(names, set( self.assertEqual(names, set(
['letsencrypt.demo', 'encryption-example.demo', 'ip-172-30-0-17'])) ['letsencrypt.demo', 'encryption-example.demo', 'ip-172-30-0-17']))
def test_find_directive(self): def test_find_dir(self):
test = self.config.find_directive( test = self.config.parser.find_dir(
apache_configurator.case_i("Listen"), "443") parser.case_i("Listen"), "443")
# This will only look in enabled hosts # This will only look in enabled hosts
test2 = self.config.find_directive( test2 = self.config.parser.find_dir(
apache_configurator.case_i("documentroot")) parser.case_i("documentroot"))
self.assertEqual(len(test), 2) self.assertEqual(len(test), 2)
self.assertEqual(len(test2), 3) self.assertEqual(len(test2), 3)
@@ -139,26 +141,26 @@ class TwoVhost80Test(unittest.TestCase):
self.assertTrue(self.config.is_site_enabled(self.vh_truth[3].filep)) self.assertTrue(self.config.is_site_enabled(self.vh_truth[3].filep))
def test_add_dir(self): def test_add_dir(self):
aug_default = "/files" + self.config.location["default"] aug_default = "/files" + self.config.parser.loc["default"]
self.config.add_dir( self.config.parser.add_dir(
aug_default, "AddDirective", "test") aug_default, "AddDirective", "test")
self.assertTrue( self.assertTrue(
self.config.find_directive("AddDirective", "test", aug_default)) self.config.parser.find_dir("AddDirective", "test", aug_default))
def test_deploy_cert(self): def test_deploy_cert(self):
self.config.deploy_cert( self.config.deploy_cert(
self.vh_truth[1], self.vh_truth[1],
"example/cert.pem", "example/key.pem", "example/cert_chain.pem") "example/cert.pem", "example/key.pem", "example/cert_chain.pem")
loc_cert = self.config.find_directive( loc_cert = self.config.parser.find_dir(
apache_configurator.case_i("sslcertificatefile"), parser.case_i("sslcertificatefile"),
re.escape("example/cert.pem"), self.vh_truth[1].path) re.escape("example/cert.pem"), self.vh_truth[1].path)
loc_key = self.config.find_directive( loc_key = self.config.parser.find_dir(
apache_configurator.case_i("sslcertificateKeyfile"), parser.case_i("sslcertificateKeyfile"),
re.escape("example/key.pem"), self.vh_truth[1].path) re.escape("example/key.pem"), self.vh_truth[1].path)
loc_chain = self.config.find_directive( loc_chain = self.config.parser.find_dir(
apache_configurator.case_i("SSLCertificateChainFile"), parser.case_i("SSLCertificateChainFile"),
re.escape("example/cert_chain.pem"), self.vh_truth[1].path) re.escape("example/cert_chain.pem"), self.vh_truth[1].path)
# Verify one directive was found in the correct file # Verify one directive was found in the correct file
@@ -175,27 +177,27 @@ class TwoVhost80Test(unittest.TestCase):
self.vh_truth[1].filep) self.vh_truth[1].filep)
def test_is_name_vhost(self): def test_is_name_vhost(self):
addr = apache_obj.Addr.fromstring("*:80") addr = obj.Addr.fromstring("*:80")
self.assertTrue(self.config.is_name_vhost(addr)) self.assertTrue(self.config.is_name_vhost(addr))
self.config.version = (2, 2) self.config.version = (2, 2)
self.assertFalse(self.config.is_name_vhost(addr)) self.assertFalse(self.config.is_name_vhost(addr))
def test_add_name_vhost(self): def test_add_name_vhost(self):
self.config.add_name_vhost("*:443") self.config.add_name_vhost("*:443")
# self.config.save(temporary=True) self.assertTrue(self.config.parser.find_dir(
self.assertTrue(self.config.find_directive(
"NameVirtualHost", re.escape("*:443"))) "NameVirtualHost", re.escape("*:443")))
def test_add_dir_to_ifmodssl(self): def test_add_dir_to_ifmodssl(self):
"""test _add_dir_to_ifmodssl. """test add_dir_to_ifmodssl.
Path must be valid before attempting to add to augeas Path must be valid before attempting to add to augeas
""" """
self.config._add_dir_to_ifmodssl( # pylint: disable=protected-access self.config.parser.add_dir_to_ifmodssl(
"/files" + self.config.location["default"], "FakeDirective", "123") parser.get_aug_path(self.config.parser.loc["default"]),
"FakeDirective", "123")
matches = self.config.find_directive("FakeDirective", "123") matches = self.config.parser.find_dir("FakeDirective", "123")
self.assertEqual(len(matches), 1) self.assertEqual(len(matches), 1)
self.assertTrue("IfModule" in matches[0]) self.assertTrue("IfModule" in matches[0])
@@ -210,16 +212,16 @@ class TwoVhost80Test(unittest.TestCase):
self.assertEqual(ssl_vhost.path, self.assertEqual(ssl_vhost.path,
"/files" + ssl_vhost.filep + "/IfModule/VirtualHost") "/files" + ssl_vhost.filep + "/IfModule/VirtualHost")
self.assertEqual(ssl_vhost.addrs, set([apache_obj.Addr.fromstring("*:443")])) self.assertEqual(ssl_vhost.addrs, set([obj.Addr.fromstring("*:443")]))
self.assertEqual(ssl_vhost.names, set(["encryption-example.demo"])) self.assertEqual(ssl_vhost.names, set(["encryption-example.demo"]))
self.assertTrue(ssl_vhost.ssl) self.assertTrue(ssl_vhost.ssl)
self.assertFalse(ssl_vhost.enabled) self.assertFalse(ssl_vhost.enabled)
self.assertTrue(self.config.find_directive( self.assertTrue(self.config.parser.find_dir(
"SSLCertificateFile", None, ssl_vhost.path)) "SSLCertificateFile", None, ssl_vhost.path))
self.assertTrue(self.config.find_directive( self.assertTrue(self.config.parser.find_dir(
"SSLCertificateKeyFile", None, ssl_vhost.path)) "SSLCertificateKeyFile", None, ssl_vhost.path))
self.assertTrue(self.config.find_directive( self.assertTrue(self.config.parser.find_dir(
"Include", self.ssl_options, ssl_vhost.path)) "Include", self.ssl_options, ssl_vhost.path))
self.assertEqual(self.config.is_name_vhost(self.vh_truth[0]), self.assertEqual(self.config.is_name_vhost(self.vh_truth[0]),
+1
View File
@@ -35,6 +35,7 @@ setup(
packages=[ packages=[
'letsencrypt', 'letsencrypt',
'letsencrypt.client', 'letsencrypt.client',
'letsencrypt.client.apache',
'letsencrypt.scripts', 'letsencrypt.scripts',
], ],
install_requires=install_requires, install_requires=install_requires,