diff --git a/letsencrypt/client/apache_obj.py b/letsencrypt/client/apache/obj.py similarity index 95% rename from letsencrypt/client/apache_obj.py rename to letsencrypt/client/apache/obj.py index b83066d81..b5bc97302 100644 --- a/letsencrypt/client/apache_obj.py +++ b/letsencrypt/client/apache/obj.py @@ -42,9 +42,11 @@ class Addr(object): return self.tup[1] def get_addr_obj(self, port): + """Return new address object with same addr and new port.""" return self.__class__((self.tup[0], port)) +# pylint: disable=too-few-public-methods class VH(object): """Represents an Apache Virtualhost. diff --git a/letsencrypt/client/apache/parser.py b/letsencrypt/client/apache/parser.py new file mode 100644 index 000000000..409c82b35 --- /dev/null +++ b/letsencrypt/client/apache/parser.py @@ -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 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 "" diff --git a/letsencrypt/client/apache_configurator.py b/letsencrypt/client/apache_configurator.py index 63c61250d..6e7d76923 100644 --- a/letsencrypt/client/apache_configurator.py +++ b/letsencrypt/client/apache_configurator.py @@ -8,13 +8,14 @@ import socket import subprocess import sys -from letsencrypt.client import apache_obj from letsencrypt.client import augeas_configurator from letsencrypt.client import challenge_util from letsencrypt.client import CONFIG from letsencrypt.client import errors 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 @@ -28,7 +29,7 @@ from letsencrypt.client import le_util # Augeas views as an error. This will just # require another check_parsing_errors() after all files are included... # (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. # Note: This protocol works for filenames with spaces in it, the sites are @@ -59,6 +60,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): parser automatically. .. 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 needs of client's are clarified with the new and developing protocol. @@ -68,7 +70,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): with the configuration :ivar float version: version of Apache :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 @@ -95,8 +97,6 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): super(ApacheConfigurator, self).__init__(direc) - self.server_root = server_root - # See if any temporary changes need to be recovered # This needs to occur before VH objects are setup... # because this will change the underlying configuration and potential @@ -107,22 +107,13 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): if os.geteuid() == 0: self.verify_setup() - # Find configuration root and make sure augeas can parse it. - self.location = self._set_locations(ssl_options) - self._parse_file(self.location["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.server_root, "sites-available/*")) + self.parser = parser.ApacheParser(self.aug, server_root, ssl_options) + # Check for errors in parsing files with Augeas + self.check_parsing_errors("httpd.aug") # Set 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 self.vhosts = self.get_virtual_hosts() # Add name_server association dict @@ -158,7 +149,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): This shouldn't happen within letsencrypt though :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 key: private key filename @@ -170,15 +161,15 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): """ path = {} - path["cert_file"] = self.find_directive(case_i( + path["cert_file"] = self.parser.find_dir(parser.case_i( "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) # Only include if a certificate chain is specified if cert_chain is not None: - path["cert_chain"] = self.find_directive( - case_i("SSLCertificateChainFile"), None, vhost.path) + path["cert_chain"] = self.parser.find_dir( + parser.case_i("SSLCertificateChainFile"), None, vhost.path) if len(path["cert_file"]) == 0 or len(path["cert_key"]) == 0: # 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) if cert_chain is not None: if len(path["cert_chain"]) == 0: - self.add_dir(vhost.path, "SSLCertificateChainFile", cert_chain) + self.parser.add_dir( + vhost.path, "SSLCertificateChainFile", cert_chain) else: self.aug.set(path["cert_chain"][0], cert_chain) @@ -216,7 +208,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): :param str name: domain 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 @@ -230,7 +222,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): return vhost # Checking for domain name in vhost address # 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: if target_addr in vhost.addrs: return vhost @@ -254,7 +246,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): :param str domain: domain name to associate :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 @@ -287,73 +279,19 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): 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): """Helper function for get_virtual_hosts(). :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')] | " "%s//*[self::directive=~regexp('%s')]" % (host.path, - case_i('ServerName'), + parser.case_i('ServerName'), host.path, - case_i('ServerAlias')))) + parser.case_i('ServerAlias')))) for name in name_match: args = self.aug.match(name + "/*") @@ -366,22 +304,22 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): :param str path: Augeas path to virtual host :returns: newly created vhost - :rtype: :class:`letsencrypt.client.apache_obj.VH` + :rtype: :class:`letsencrypt.client.apache.obj.VH` """ addrs = set() args = self.aug.match(path + "/arg") 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 - if self.find_directive( - case_i("SSLEngine"), case_i("on"), path): + if self.parser.find_dir( + parser.case_i("SSLEngine"), parser.case_i("on"), path): is_ssl = True filename = get_file_path(path) 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) return vhost @@ -389,7 +327,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): def get_virtual_hosts(self): """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 :rtype: list @@ -397,7 +335,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): # Search sites-available, httpd.conf for possible virtual hosts paths = self.aug.match( ("/files%ssites-available//*[label()=~regexp('%s')]" % - (self.server_root, case_i('VirtualHost')))) + (self.parser.root, parser.case_i('VirtualHost')))) vhs = [] for path in paths: @@ -425,8 +363,9 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): # search for NameVirtualHost directive for ip_addr # note ip_addr can be FQDN although Apache does not recommend it return (self.version >= (2, 4) or - self.find_directive( - case_i("NameVirtualHost"), case_i(str(target_addr)))) + self.parser.find_dir( + parser.case_i("NameVirtualHost"), + parser.case_i(str(target_addr)))) def add_name_vhost(self, addr): """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 """ - path = self._add_dir_to_ifmodssl( - get_aug_path(self.location["name"]), "NameVirtualHost", str(addr)) + path = self.parser.add_dir_to_ifmodssl( + parser.get_aug_path( + self.parser.loc["name"]), "NameVirtualHost", str(addr)) self.save_notes += "Setting %s to be NameBasedVirtualHost\n" % addr 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): """Prepare the server for HTTPS. @@ -475,18 +394,18 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): # Check for Listen 443 # Note: This could be made to also look for ip:443 combo # 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("Setting the Apache Server to Listen on port 443") - path = self._add_dir_to_ifmodssl( - get_aug_path(self.location["listen"]), "Listen", "443") + path = self.parser.add_dir_to_ifmodssl( + parser.get_aug_path(self.parser.loc["listen"]), "Listen", "443") self.save_notes += "Added Listen 443 directive to %s\n" % path def make_server_sni_ready(self, vhost, default_addr="*:443"): """Checks to see if the server is ready for SNI challenges. :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 @@ -509,178 +428,6 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): "based virtual host", addr) self.add_name_vhost(addr) - def _get_ifmod(self, aug_conf_path, mod): - """Returns the path to 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): """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 :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 - :rtype: :class:`letsencrypt.client.apache_obj.VH` + :rtype: :class:`letsencrypt.client.apache.obj.VH` """ avail_fp = nonssl_vhost.filep @@ -726,10 +473,10 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): # change address to address:443 addr_match = "/files%s//* [label()=~regexp('%s')]/arg" 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)): - ssl_addr_arg = apache_obj.Addr.fromstring( + ssl_addr_arg = obj.Addr.fromstring( str(self.aug.get(ssl_addr_p[i]))) ssl_addr_arg.set_port("443") self.aug.set(ssl_addr_p[i], str(ssl_addr_arg)) @@ -737,16 +484,16 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): # Add directives 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: logging.error("Error: should only be one vhost in %s", avail_fp) sys.exit(1) - self.add_dir(vh_p[0], "SSLCertificateFile", - "/etc/ssl/certs/ssl-cert-snakeoil.pem") - self.add_dir(vh_p[0], "SSLCertificateKeyFile", - "/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], "SSLCertificateFile", + "/etc/ssl/certs/ssl-cert-snakeoil.pem") + self.parser.add_dir(vh_p[0], "SSLCertificateKeyFile", + "/etc/ssl/private/ssl-cert-snakeoil.key") + self.parser.add_dir(vh_p[0], "Include", self.parser.loc["ssl_options"]) # Log actions and create save notes 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 :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) - :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 @@ -812,9 +559,9 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): logging.debug("Unknown redirect exists for this vhost") return False, general_v # Add directives to server - self.add_dir(general_v.path, "RewriteEngine", "On") - self.add_dir(general_v.path, - "RewriteRule", CONFIG.REWRITE_HTTPS_ARGS) + self.parser.add_dir(general_v.path, "RewriteEngine", "On") + self.parser.add_dir( + general_v.path, "RewriteRule", CONFIG.REWRITE_HTTPS_ARGS) self.save_notes += ('Redirecting host in %s to ssl vhost in %s\n' % (general_v.filep, ssl_vhost.filep)) self.save() @@ -834,16 +581,16 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): -1 is also returned in case of no redirection/rewrite directives :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 :rtype: bool, int """ - rewrite_path = self.find_directive( - case_i("RewriteRule"), None, vhost.path) - redirect_path = self.find_directive( - case_i("Redirect"), None, vhost.path) + rewrite_path = self.parser.find_dir( + parser.case_i("RewriteRule"), None, vhost.path) + redirect_path = self.parser.find_dir( + parser.case_i("Redirect"), None, vhost.path) if redirect_path: # "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. :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 - :rtype: (bool, :class:`letsencrypt.client.apache_obj.VH`) + :rtype: (bool, :class:`letsencrypt.client.apache.obj.VH`) """ # 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_filepath = ("%ssites-available/%s" % - (self.server_root, redirect_filename)) + (self.parser.root, redirect_filename)) # Register the new file that will be created # Note: always register the creation before writing to ensure file will @@ -928,8 +675,8 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): self.aug.load() # Make a new vhost data structure and add it to the lists - new_fp = self.server_root + "sites-available/" + redirect_filename - new_vhost = self._create_vhost(get_aug_path(new_fp)) + new_fp = self.parser.root + "sites-available/" + redirect_filename + new_vhost = self._create_vhost(parser.get_aug_path(new_fp)) self.vhosts.append(new_vhost) # 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 :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 :rtype: TODO @@ -984,18 +731,18 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): Consider changing this into a dict 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 - :rtype: :class:`letsencrypt.client.apache_obj.VH` or None + :rtype: :class:`letsencrypt.client.apache.obj.VH` 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 == apache_obj.Addr.fromstring("_default_:443"): - ssl_addrs = [apache_obj.Addr.fromstring("*:443")] + if ssl_addrs == obj.Addr.fromstring("_default_:443"): + ssl_addrs = [obj.Addr.fromstring("*:443")] for vhost in self.vhosts: found = 0 @@ -1038,10 +785,10 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): for vhost in self.vhosts: if vhost.ssl: - cert_path = self.find_directive( - case_i("SSLCertificateFile"), None, vhost.path) - key_path = self.find_directive( - case_i("SSLCertificateKeyFile"), None, vhost.path) + cert_path = self.parser.find_dir( + parser.case_i("SSLCertificateFile"), None, vhost.path) + key_path = self.parser.find_dir( + parser.case_i("SSLCertificateKeyFile"), None, vhost.path) # Can be removed once find directive can return ordered results if len(cert_path) != 1 or len(key_path) != 1: @@ -1066,7 +813,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): :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): if os.path.realpath(enabled_dir + entry) == avail_fp: return True @@ -1081,7 +828,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): .. todo:: Make sure link is not broken... :param vhost: vhost to enable - :type vhost: :class:`letsencrypt.client.apache_obj.VH` + :type vhost: :class:`letsencrypt.client.apache.obj.VH` :returns: Success :rtype: bool @@ -1092,7 +839,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): if "/sites-available/" in vhost.filep: 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) os.symlink(vhost.filep, enabled_path) vhost.enabled = True @@ -1101,82 +848,6 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): return True 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 """Restarts apache server. @@ -1186,19 +857,6 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): """ 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): """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` :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 @@ -1398,7 +1056,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): list_sni_tuple[idx][2], lis, dvsni_key.file) config_text += " \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) 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 """ - if len(self.find_directive( - case_i("Include"), CONFIG.APACHE_CHALLENGE_CONF)) == 0: + if len(self.parser.find_dir( + parser.case_i("Include"), CONFIG.APACHE_CHALLENGE_CONF)) == 0: # print "Including challenge virtual host(s)" - self.add_dir(get_aug_path(main_config), - "Include", CONFIG.APACHE_CHALLENGE_CONF) + self.parser.add_dir(parser.get_aug_path(main_config), + "Include", CONFIG.APACHE_CHALLENGE_CONF) def get_config_text(self, nonce, ip_addrs, dvsni_key_file): """Chocolate virtual server configuration text :param str nonce: hex form of nonce :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 :returns: virtual host configuration text @@ -1439,7 +1097,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): "\n" "LimitRequestBody 1048576\n" "\n" - "Include " + self.location["ssl_options"] + "\n" + "Include " + self.parser.loc["ssl_options"] + "\n" "SSLCertificateFile " + self.dvsni_get_cert_file(nonce) + "\n" "SSLCertificateKeyFile " + dvsni_key_file + "\n" "\n" @@ -1538,22 +1196,6 @@ def apache_restart(): 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): """Get file path from augeas_vhost_path. @@ -1580,94 +1222,3 @@ def get_file_path(vhost_path): continue break 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() diff --git a/letsencrypt/client/augeas_configurator.py b/letsencrypt/client/augeas_configurator.py index 0ad813c8a..15fb84b72 100644 --- a/letsencrypt/client/augeas_configurator.py +++ b/letsencrypt/client/augeas_configurator.py @@ -15,8 +15,6 @@ from letsencrypt.client import le_util class AugeasConfigurator(configurator.Configurator): """Base Augeas Configurator class. - .. todo:: Fix generic exception handling. - :ivar aug: Augeas object :type aug: :class:`augeas.Augeas` diff --git a/letsencrypt/client/tests/apache_configurator_test.py b/letsencrypt/client/tests/apache_configurator_test.py index 8e745b7d7..2c8742731 100644 --- a/letsencrypt/client/tests/apache_configurator_test.py +++ b/letsencrypt/client/tests/apache_configurator_test.py @@ -8,12 +8,12 @@ import unittest import mock -from letsencrypt.client import apache_obj from letsencrypt.client import apache_configurator from letsencrypt.client import CONFIG from letsencrypt.client import display from letsencrypt.client import errors - +from letsencrypt.client.apache import obj +from letsencrypt.client.apache import parser UBUNTU_CONFIGS = pkg_resources.resource_filename( __name__, "testdata/debian_apache_2_4") @@ -62,24 +62,24 @@ class TwoVhost80Test(unittest.TestCase): self.temp_dir, "two_vhost_80/apache2/sites-available") aug_pre = "/files" + prefix self.vh_truth = [ - apache_obj.VH( + obj.VH( os.path.join(prefix, "encryption-example.conf"), 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"])), - apache_obj.VH( + obj.VH( os.path.join(prefix, "default-ssl.conf"), os.path.join(aug_pre, "default-ssl.conf/IfModule/VirtualHost"), - set([apache_obj.Addr.fromstring("_default_:443")]), True, False), - apache_obj.VH( + set([obj.Addr.fromstring("_default_:443")]), True, False), + obj.VH( os.path.join(prefix, "000-default.conf"), 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"])), - apache_obj.VH( + obj.VH( os.path.join(prefix, "letsencrypt.conf"), 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"])), ] @@ -97,7 +97,9 @@ class TwoVhost80Test(unittest.TestCase): """ file_path = os.path.join( 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 matches = self.config.aug.match( @@ -110,12 +112,12 @@ class TwoVhost80Test(unittest.TestCase): self.assertEqual(names, set( ['letsencrypt.demo', 'encryption-example.demo', 'ip-172-30-0-17'])) - def test_find_directive(self): - test = self.config.find_directive( - apache_configurator.case_i("Listen"), "443") + def test_find_dir(self): + test = self.config.parser.find_dir( + parser.case_i("Listen"), "443") # This will only look in enabled hosts - test2 = self.config.find_directive( - apache_configurator.case_i("documentroot")) + test2 = self.config.parser.find_dir( + parser.case_i("documentroot")) self.assertEqual(len(test), 2) 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)) def test_add_dir(self): - aug_default = "/files" + self.config.location["default"] - self.config.add_dir( + aug_default = "/files" + self.config.parser.loc["default"] + self.config.parser.add_dir( aug_default, "AddDirective", "test") self.assertTrue( - self.config.find_directive("AddDirective", "test", aug_default)) + self.config.parser.find_dir("AddDirective", "test", aug_default)) def test_deploy_cert(self): self.config.deploy_cert( self.vh_truth[1], "example/cert.pem", "example/key.pem", "example/cert_chain.pem") - loc_cert = self.config.find_directive( - apache_configurator.case_i("sslcertificatefile"), + loc_cert = self.config.parser.find_dir( + parser.case_i("sslcertificatefile"), re.escape("example/cert.pem"), self.vh_truth[1].path) - loc_key = self.config.find_directive( - apache_configurator.case_i("sslcertificateKeyfile"), + loc_key = self.config.parser.find_dir( + parser.case_i("sslcertificateKeyfile"), re.escape("example/key.pem"), self.vh_truth[1].path) - loc_chain = self.config.find_directive( - apache_configurator.case_i("SSLCertificateChainFile"), + loc_chain = self.config.parser.find_dir( + parser.case_i("SSLCertificateChainFile"), re.escape("example/cert_chain.pem"), self.vh_truth[1].path) # Verify one directive was found in the correct file @@ -175,27 +177,27 @@ class TwoVhost80Test(unittest.TestCase): self.vh_truth[1].filep) 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.config.version = (2, 2) self.assertFalse(self.config.is_name_vhost(addr)) def test_add_name_vhost(self): self.config.add_name_vhost("*:443") - # self.config.save(temporary=True) - self.assertTrue(self.config.find_directive( + self.assertTrue(self.config.parser.find_dir( "NameVirtualHost", re.escape("*:443"))) 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 """ - self.config._add_dir_to_ifmodssl( # pylint: disable=protected-access - "/files" + self.config.location["default"], "FakeDirective", "123") + self.config.parser.add_dir_to_ifmodssl( + 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.assertTrue("IfModule" in matches[0]) @@ -210,16 +212,16 @@ class TwoVhost80Test(unittest.TestCase): self.assertEqual(ssl_vhost.path, "/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.assertTrue(ssl_vhost.ssl) self.assertFalse(ssl_vhost.enabled) - self.assertTrue(self.config.find_directive( + self.assertTrue(self.config.parser.find_dir( "SSLCertificateFile", None, ssl_vhost.path)) - self.assertTrue(self.config.find_directive( + self.assertTrue(self.config.parser.find_dir( "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)) self.assertEqual(self.config.is_name_vhost(self.vh_truth[0]), diff --git a/setup.py b/setup.py index 24ff3752d..e84906910 100755 --- a/setup.py +++ b/setup.py @@ -35,6 +35,7 @@ setup( packages=[ 'letsencrypt', 'letsencrypt.client', + 'letsencrypt.client.apache', 'letsencrypt.scripts', ], install_requires=install_requires,