From 0d69e5cff416311f6f0ea06d891c807fed119c7e Mon Sep 17 00:00:00 2001 From: James Kasten Date: Wed, 15 Jul 2015 14:34:24 -0700 Subject: [PATCH] Support dvsni_port, reorganize dvsni --- .../letsencrypt_apache/configurator.py | 57 ++++++++--------- .../letsencrypt_apache/dvsni.py | 63 ++++++++++++------- letsencrypt-apache/letsencrypt_apache/obj.py | 34 ++++++++++ 3 files changed, 99 insertions(+), 55 deletions(-) diff --git a/letsencrypt-apache/letsencrypt_apache/configurator.py b/letsencrypt-apache/letsencrypt_apache/configurator.py index 49b234ee2..7edcb8cc8 100644 --- a/letsencrypt-apache/letsencrypt_apache/configurator.py +++ b/letsencrypt-apache/letsencrypt_apache/configurator.py @@ -53,25 +53,13 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): # pylint: disable=too-many-instance-attributes,too-many-public-methods """Apache configurator. - State of Configurator: This code has been tested under Ubuntu 12.04 - Apache 2.2 and this code works for Ubuntu 14.04 Apache 2.4. Further - notes below. - - This class was originally developed for Apache 2.2 and I have been slowly - transitioning the codebase to work with all of the 2.4 features. - I have implemented most of the changes... the missing ones are - mod_ssl.c vs ssl_mod, and I need to account for configuration variables. - This class can adequately configure most typical configurations but - is not ready to handle very complex configurations. + State of Configurator: This code has been been tested and built for Ubuntu + 14.04 Apache 2.4 and it works for Ubuntu 12.04 Apache 2.2 .. todo:: Verify permissions on configuration root... it is easier than checking permissions on each of the relative directories and less error prone. - - The API of this class will change in the coming weeks as the exact - needs of clients are clarified with the new and developing protocol. - :ivar config: Configuration. :type config: :class:`~letsencrypt.interfaces.IConfig` @@ -204,6 +192,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): else: self.aug.set(path["chain_path"][-1], chain_path) + # Save notes about the transaction that took place self.save_notes += ("Changed vhost at %s with addresses of %s\n" % (vhost.filep, ", ".join(str(addr) for addr in vhost.addrs))) @@ -360,7 +349,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): addrs = set() args = self.aug.match(path + "/arg") for arg in args: - addrs.add(common.Addr.fromstring(self.parser.get_arg(arg))) + addrs.add(obj.Addr.fromstring(self.parser.get_arg(arg))) is_ssl = False if self.parser.find_dir( @@ -400,7 +389,8 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): now NameVirtualHosts. If version is earlier than 2.4, check if addr has a NameVirtualHost directive in the Apache config - :param str target_addr: vhost address ie. \*:443 + :param target_addr: vhost address + :type target_addr: :class:~letsencrypt_apache.obj.Addr :returns: Success :rtype: bool @@ -419,7 +409,8 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): def add_name_vhost(self, addr): """Adds NameVirtualHost directive for given address. - :param str addr: Address that will be added as NameVirtualHost directive + :param addr: Address that will be added as NameVirtualHost directive + :type addr: :class:~letsencrypt_apache.obj.Addr """ path = self.parser.add_dir_to_ifmodssl( @@ -435,7 +426,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): Make sure that the ssl_module is loaded and that the server is appropriately listening on port. - :param int port: Port to listen on + :param str port: Port to listen on """ if "ssl_module" not in self.parser.modules: @@ -444,30 +435,34 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): # Check for Listen # Note: This could be made to also look for ip:443 combo - if not self.parser.find_dir(parser.case_i("Listen"), str(port)): + if not self.parser.find_dir(parser.case_i("Listen"), port): logger.debug("No Listen {0} directive found. Setting the " "Apache Server to Listen on port {0}".format(port)) path = self.parser.add_dir_to_ifmodssl( parser.get_aug_path( - self.parser.loc["listen"]), "Listen", str(port)) - self.save_notes += "Added Listen %d directive to %s\n" % ( + self.parser.loc["listen"]), "Listen", port) + self.save_notes += "Added Listen %s directive to %s\n" % ( port, path) - def make_server_sni_ready(self, vhost, default_addr="*:443"): + def make_addrs_sni_ready( + self, addrs, default_addr=obj.Addr(("*", "443"))): """Checks to see if the server is ready for SNI challenges. - :param vhost: VirtualHost to check SNI compatibility - :type vhost: :class:`~letsencrypt_apache.obj.VirtualHost` + :param addrs: Addresses to check SNI compatibility + :type addrs: :class:`~letsencrypt_apache.obj.Addr` - :param str default_addr: TODO - investigate function further + :param default_addr: TODO - investigate function further + :type default_addr: :class:~letsencrypt_apache.obj.Addr """ # Version 2.4 and later are automatically SNI ready. if self.version >= (2, 4): return + + # TODO: Review this 3-year old demo code # Check for NameVirtualHost # First see if any of the vhost addresses is a _default_ addr - for addr in vhost.addrs: + for addr in addrs: if addr.get_addr() == "_default_": if not self.is_name_vhost(default_addr): logger.debug("Setting all VirtualHosts on %s to be " @@ -475,7 +470,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): self.add_name_vhost(default_addr) # No default addresses... so set each one individually - for addr in vhost.addrs: + for addr in addrs: if not self.is_name_vhost(addr): logger.debug("Setting VirtualHost at %s to be a name " "based virtual host", addr) @@ -582,7 +577,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): ssl_addr_p = self.aug.match(vh_path + "/arg") for addr in ssl_addr_p: - old_addr = common.Addr.fromstring( + old_addr = obj.Addr.fromstring( str(self.parser.get_arg(addr))) ssl_addr = old_addr.get_addr_obj("443") self.aug.set(addr, str(ssl_addr)) @@ -880,8 +875,8 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): # Instead... should look for vhost of the form *:80 # Should we prompt the user? ssl_addrs = ssl_vhost.addrs - if ssl_addrs == common.Addr.fromstring("_default_:443"): - ssl_addrs = [common.Addr.fromstring("*:443")] + if ssl_addrs == obj.Addr.fromstring("_default_:443"): + ssl_addrs = [obj.Addr.fromstring("*:443")] for vhost in self.vhosts: found = 0 @@ -975,7 +970,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): if vhost.ssl: # TODO: Make this based on addresses - self._prepare_server_https(443) + self._prepare_server_https("443") if self.save_notes: self.save("Enabled TLS for Apache") diff --git a/letsencrypt-apache/letsencrypt_apache/dvsni.py b/letsencrypt-apache/letsencrypt_apache/dvsni.py index eace6ce90..ac0a3039a 100644 --- a/letsencrypt-apache/letsencrypt_apache/dvsni.py +++ b/letsencrypt-apache/letsencrypt_apache/dvsni.py @@ -3,6 +3,7 @@ import os from letsencrypt.plugins import common +from letsencrypt_apache import obj from letsencrypt_apache import parser @@ -59,21 +60,6 @@ class ApacheDvsni(common.Dvsni): # About to make temporary changes to the config self.configurator.save() - addresses = [] - default_addr = "*:443" - for achall in self.achalls: - vhost = self.configurator.choose_vhost(achall.domain) - - # TODO - @jdkasten review this code to make sure it makes sense - self.configurator.make_server_sni_ready(vhost, default_addr) - - for addr in vhost.addrs: - if "_default_" == addr.get_addr(): - addresses.append([default_addr]) - break - else: - addresses.append(list(vhost.addrs)) - responses = [] # Create all of the challenge certs @@ -81,29 +67,37 @@ class ApacheDvsni(common.Dvsni): responses.append(self._setup_challenge_cert(achall)) # Setup the configuration - self._mod_config(addresses) + dvsni_addrs = self._mod_config() + + self.configurator.make_addrs_sni_ready(dvsni_addrs) # Prepare the server for HTTPS - # TODO: Base on addresses - self.configurator._prepare_https_server(443) + self.configurator._prepare_https_server( + str(self.configurator.config.dvsni_port)) # Save reversible changes self.configurator.save("SNI Challenge", True) return responses - def _mod_config(self, ll_addrs): + def _mod_config(self): """Modifies Apache config files to include challenge vhosts. Result: Apache config includes virtual servers for issued challs - :param list ll_addrs: list of list of `~.common.Addr` to apply + :returns: All DVSNI addresses used + :rtype: set """ - # TODO: Use ip address of existing vhost instead of relying on FQDN + dvsni_addrs = set() config_text = "\n" - for idx, lis in enumerate(ll_addrs): - config_text += self._get_config_text(self.achalls[idx], lis) + + for achall in self.achalls: + achall_addrs = self.get_dvsni_addrs(achall) + dvsni_addrs.update(achall_addrs) + + config_text += self._get_config_text(self.achalls, achall_addrs) + config_text += "\n" self._conf_include_check(self.configurator.parser.loc["default"]) @@ -113,6 +107,27 @@ class ApacheDvsni(common.Dvsni): with open(self.challenge_conf, "w") as new_conf: new_conf.write(config_text) + return dvsni_addrs + + def get_dvsni_addrs(self, achall): + """Return the Apache addresses needed for DVSNI.""" + vhost = self.configurator.choose_vhost(achall.domain) + + # TODO: Checkout _default_ rules. + # TODO: Need to separate out test mode and normal mode for DVSNI addrs + dvsni_addrs = set() + default_addr = obj.Addr(("*", self.configurator.config.dvsni_port)) + + for addr in vhost.addrs: + # I don't think there can be two _default_ namebasedvhosts + if "_default_" == addr.get_addr(): + dvsni_addrs.add(default_addr) + else: + dvsni_addrs.add( + addr.get_sni_addr(self.configurator.config.dvsni_port)) + + return dvsni_addrs + def _conf_include_check(self, main_config): """Adds DVSNI challenge conf file into configuration. @@ -136,7 +151,7 @@ class ApacheDvsni(common.Dvsni): :type achall: :class:`letsencrypt.achallenges.DVSNI` :param list ip_addrs: addresses of challenged domain - :class:`list` of type `~.common.Addr` + :class:`list` of type `~.obj.Addr` :returns: virtual host configuration text :rtype: str diff --git a/letsencrypt-apache/letsencrypt_apache/obj.py b/letsencrypt-apache/letsencrypt_apache/obj.py index 13e00edd8..956b6999f 100644 --- a/letsencrypt-apache/letsencrypt_apache/obj.py +++ b/letsencrypt-apache/letsencrypt_apache/obj.py @@ -1,4 +1,38 @@ """Module contains classes used by the Apache Configurator.""" +from letsencrypt.plugins import common + +class Addr(common.Addr): + + def __eq__(self, other): + """This is defined as equalivalent within Apache. + + ip_addr:* == ip_addr + + """ + if isinstance(other, self.__class__): + return ((self.tup == other.tup) or + (self.tup[0] == other.tup[0] + and self.is_wildcard() and other.is_wildcard())) + return False + + def is_wildcard(self): + return tup[1] == "*" or not tup[1] + + def get_sni_addr(self, port): + """Returns the least specific address that resolves on the port. + + Example: + 1.2.3.4:443 -> 1.2.3.4: + 1.2.3.4:* -> 1.2.3.4:* + + :param str port: Desired port + + """ + if self.is_wildcard(): + return self + + return self.get_addr_obj(port) + class VirtualHost(object): # pylint: disable=too-few-public-methods