def get_all_certs_keys(self): """Find all existing keys, certs from configuration. Retrieve all certs and keys set in VirtualHosts on the Apache server :returns: list of tuples with form [(cert, key, path)] cert - str path to certificate file key - str path to associated key file path - File path to configuration file. :rtype: list """ c_k = set() for vhost in self.vhosts: if vhost.ssl: 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: logger.error("Too many cert or key directives in vhost %s", vhost.filep) errors.MisconfigurationError( "Too many cert/key directives in vhost") cert = os.path.abspath(self.aug.get(cert_path[0])) key = os.path.abspath(self.aug.get(key_path[0])) c_k.add((cert, key, get_file_path(cert_path[0]))) return c_k
def test_find_dir(self): from letsencrypt_apache.parser import case_i test = self.parser.find_dir(case_i("Listen"), "443") # This will only look in enabled hosts test2 = self.parser.find_dir(case_i("documentroot")) self.assertEqual(len(test), 2) self.assertEqual(len(test2), 3)
def test_deploy_cert(self): # Get the default 443 vhost self.config.assoc["random.demo"] = self.vh_truth[1] self.config.deploy_cert( "random.demo", "example/cert.pem", "example/key.pem", "example/cert_chain.pem") self.config.save() 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.parser.find_dir( parser.case_i("sslcertificateKeyfile"), re.escape("example/key.pem"), self.vh_truth[1].path) 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 self.assertEqual(len(loc_cert), 1) self.assertEqual(configurator.get_file_path(loc_cert[0]), self.vh_truth[1].filep) self.assertEqual(len(loc_key), 1) self.assertEqual(configurator.get_file_path(loc_key[0]), self.vh_truth[1].filep) self.assertEqual(len(loc_chain), 1) self.assertEqual(configurator.get_file_path(loc_chain[0]), self.vh_truth[1].filep)
def deploy_cert(self, domain, cert_path, key_path, chain_path=None): """Deploys certificate to specified virtual host. Currently tries to find the last directives to deploy the cert in the VHost associated with the given domain. If it can't find the directives, it searches the "included" confs. The function verifies that it has located the three directives and finally modifies them to point to the correct destination. After the certificate is installed, the VirtualHost is enabled if it isn't already. .. todo:: Make sure last directive is changed .. todo:: Might be nice to remove chain directive if none exists This shouldn't happen within letsencrypt though """ vhost = self.choose_vhost(domain) path = {} path["cert_path"] = self.parser.find_dir( parser.case_i("SSLCertificateFile"), None, vhost.path) path["cert_key"] = self.parser.find_dir( parser.case_i("SSLCertificateKeyFile"), None, vhost.path) # Only include if a certificate chain is specified if chain_path is not None: path["chain_path"] = self.parser.find_dir( parser.case_i("SSLCertificateChainFile"), None, vhost.path) if not path["cert_path"] or not path["cert_key"]: # Throw some can't find all of the directives error" logger.warn( "Cannot find a cert or key directive in %s. " "VirtualHost was not modified", vhost.path) # Presumably break here so that the virtualhost is not modified return False logger.info("Deploying Certificate to VirtualHost %s", vhost.filep) self.aug.set(path["cert_path"][0], cert_path) self.aug.set(path["cert_key"][0], key_path) if chain_path is not None: if not path["chain_path"]: self.parser.add_dir(vhost.path, "SSLCertificateChainFile", chain_path) else: self.aug.set(path["chain_path"][0], chain_path) self.save_notes += ( "Changed vhost at %s with addresses of %s\n" % (vhost.filep, ", ".join(str(addr) for addr in vhost.addrs))) self.save_notes += "\tSSLCertificateFile %s\n" % cert_path self.save_notes += "\tSSLCertificateKeyFile %s\n" % key_path if chain_path is not None: self.save_notes += "\tSSLCertificateChainFile %s\n" % chain_path # Make sure vhost is enabled if not vhost.enabled: self.enable_site(vhost)
def deploy_cert(self, domain, cert_path, key_path, chain_path=None): """Deploys certificate to specified virtual host. Currently tries to find the last directives to deploy the cert in the VHost associated with the given domain. If it can't find the directives, it searches the "included" confs. The function verifies that it has located the three directives and finally modifies them to point to the correct destination. After the certificate is installed, the VirtualHost is enabled if it isn't already. .. todo:: Make sure last directive is changed .. todo:: Might be nice to remove chain directive if none exists This shouldn't happen within letsencrypt though """ vhost = self.choose_vhost(domain) path = {} path["cert_path"] = self.parser.find_dir(parser.case_i( "SSLCertificateFile"), None, vhost.path) path["cert_key"] = self.parser.find_dir(parser.case_i( "SSLCertificateKeyFile"), None, vhost.path) # Only include if a certificate chain is specified if chain_path is not None: path["chain_path"] = self.parser.find_dir( parser.case_i("SSLCertificateChainFile"), None, vhost.path) if not path["cert_path"] or not path["cert_key"]: # Throw some can't find all of the directives error" logger.warn( "Cannot find a cert or key directive in %s. " "VirtualHost was not modified", vhost.path) # Presumably break here so that the virtualhost is not modified return False logger.info("Deploying Certificate to VirtualHost %s", vhost.filep) self.aug.set(path["cert_path"][0], cert_path) self.aug.set(path["cert_key"][0], key_path) if chain_path is not None: if not path["chain_path"]: self.parser.add_dir( vhost.path, "SSLCertificateChainFile", chain_path) else: self.aug.set(path["chain_path"][0], chain_path) self.save_notes += ("Changed vhost at %s with addresses of %s\n" % (vhost.filep, ", ".join(str(addr) for addr in vhost.addrs))) self.save_notes += "\tSSLCertificateFile %s\n" % cert_path self.save_notes += "\tSSLCertificateKeyFile %s\n" % key_path if chain_path is not None: self.save_notes += "\tSSLCertificateChainFile %s\n" % chain_path # Make sure vhost is enabled if not vhost.enabled: self.enable_site(vhost)
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_apache.obj.VirtualHost` """ name_match = self.aug.match( ("%s//*[self::directive=~regexp('%s')] | " "%s//*[self::directive=~regexp('%s')]" % (host.path, parser.case_i("ServerName"), host.path, parser.case_i("ServerAlias")))) for name in name_match: args = self.aug.match(name + "/*") for arg in args: host.add_name(self.aug.get(arg))
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_apache.obj.VirtualHost` """ name_match = self.aug.match(("%s//*[self::directive=~regexp('%s')] | " "%s//*[self::directive=~regexp('%s')]" % (host.path, parser.case_i("ServerName"), host.path, parser.case_i("ServerAlias")))) for name in name_match: args = self.aug.match(name + "/*") for arg in args: host.add_name(self.aug.get(arg))
def is_name_vhost(self, target_addr): r"""Returns if vhost is a name based vhost NameVirtualHost was deprecated in Apache 2.4 as all VirtualHosts are 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 :returns: Success :rtype: bool """ # Mixed and matched wildcard NameVirtualHost with VirtualHost # behavior is undefined. Make sure that an exact match exists # 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.parser.find_dir( parser.case_i("NameVirtualHost"), parser.case_i(str(target_addr))))
def _existing_redirect(self, vhost): """Checks to see if existing redirect is in place. Checks to see if virtualhost already contains a rewrite or redirect returns boolean, integer The boolean indicates whether the redirection exists... The integer has the following code: 0 - Existing letsencrypt https rewrite rule is appropriate and in place 1 - Virtual host contains a Redirect directive 2 - Virtual host contains an unknown RewriteRule -1 is also returned in case of no redirection/rewrite directives :param vhost: vhost to check :type vhost: :class:`~letsencrypt_apache.obj.VirtualHost` :returns: Success, code value... see documentation :rtype: bool, int """ 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" return True, 1 if not rewrite_path: # "No existing redirection for virtualhost" return False, -1 if len(rewrite_path) == len(constants.REWRITE_HTTPS_ARGS): for idx, match in enumerate(rewrite_path): if (self.aug.get(match) != constants.REWRITE_HTTPS_ARGS[idx]): # Not a letsencrypt https rewrite return True, 2 # Existing letsencrypt https rewrite rule is in place return True, 0 # Rewrite path exists but is not a letsencrypt https rule return True, 2
def _conf_include_check(self, main_config): """Add TLS-SNI-01 challenge conf file into configuration. Adds TLS-SNI-01 challenge include file if it does not already exist within mainConfig :param str main_config: file path to main user apache config file """ if len(self.configurator.parser.find_dir(parser.case_i("Include"), self.challenge_conf)) == 0: # print "Including challenge virtual host(s)" self.configurator.parser.add_dir(parser.get_aug_path(main_config), "Include", self.challenge_conf)
def _create_vhost(self, path): """Used by get_virtual_hosts to create vhost objects :param str path: Augeas path to virtual host :returns: newly created vhost :rtype: :class:`~letsencrypt_apache.obj.VirtualHost` """ addrs = set() args = self.aug.match(path + "/arg") for arg in args: addrs.add(common.Addr.fromstring(self.aug.get(arg))) is_ssl = False 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 = obj.VirtualHost(filename, path, addrs, is_ssl, is_enabled) self._add_servernames(vhost) return vhost
def _create_vhost(self, path): """Used by get_virtual_hosts to create vhost objects :param str path: Augeas path to virtual host :returns: newly created vhost :rtype: :class:`~letsencrypt_apache.obj.VirtualHost` """ addrs = set() args = self.aug.match(path + "/arg") for arg in args: addrs.add(common.Addr.fromstring(self.aug.get(arg))) is_ssl = False 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 = obj.VirtualHost(filename, path, addrs, is_ssl, is_enabled) self._add_servernames(vhost) return vhost
def _conf_include_check(self, main_config): """Add TLS-SNI-01 challenge conf file into configuration. Adds TLS-SNI-01 challenge include file if it does not already exist within mainConfig :param str main_config: file path to main user apache config file """ if len( self.configurator.parser.find_dir(parser.case_i("Include"), self.challenge_conf)) == 0: # print "Including challenge virtual host(s)" self.configurator.parser.add_dir(parser.get_aug_path(main_config), "Include", self.challenge_conf)
def get_virtual_hosts(self): """Returns list of virtual hosts found in the Apache configuration. :returns: List of :class:`~letsencrypt_apache.obj.VirtualHost` objects found in configuration :rtype: list """ # Search sites-available, httpd.conf for possible virtual hosts paths = self.aug.match( ("/files%s/sites-available//*[label()=~regexp('%s')]" % (self.parser.root, parser.case_i("VirtualHost")))) vhs = [] for path in paths: vhs.append(self._create_vhost(path)) return vhs
def _prepare_server_https(self): """Prepare the server for HTTPS. Make sure that the ssl_module is loaded and that the server is appropriately listening on port 443. """ if not self.mod_loaded("ssl_module"): logger.info("Loading mod_ssl into Apache Server") self.enable_mod("ssl") # 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.parser.find_dir(parser.case_i("Listen"), "443")) == 0: logger.debug("No Listen 443 directive found. Setting the " "Apache Server to Listen on port 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_vhost_ssl(self, nonssl_vhost): # pylint: disable=too-many-locals """Makes an ssl_vhost version of a nonssl_vhost. Duplicates vhost and adds default ssl options New vhost will reside as (nonssl_vhost.path) + ``letsencrypt_apache.constants.CLI_DEFAULTS["le_vhost_ext"]`` .. note:: This function saves the configuration :param nonssl_vhost: Valid VH that doesn't have SSLEngine on :type nonssl_vhost: :class:`~letsencrypt_apache.obj.VirtualHost` :returns: SSL vhost :rtype: :class:`~letsencrypt_apache.obj.VirtualHost` :raises .errors.PluginError: If more than one virtual host is in the file or if plugin is unable to write/read vhost files. """ avail_fp = nonssl_vhost.filep ssl_fp = self._get_ssl_vhost_path(avail_fp) self._copy_create_ssl_vhost_skeleton(avail_fp, ssl_fp) # Reload augeas to take into account the new vhost self.aug.load() # Get Vhost augeas path for new vhost vh_p = self.aug.match("/files%s//* [label()=~regexp('%s')]" % (ssl_fp, parser.case_i("VirtualHost"))) if len(vh_p) != 1: logger.error("Error: should only be one vhost in %s", avail_fp) raise errors.PluginError("Only one vhost per file is allowed") else: # This simplifies the process vh_p = vh_p[0] # Update Addresses self._update_ssl_vhosts_addrs(vh_p) # Add directives self._add_dummy_ssl_directives(vh_p) # Log actions and create save notes logger.info("Created an SSL vhost at %s", ssl_fp) self.save_notes += "Created ssl vhost at %s\n" % ssl_fp self.save() # We know the length is one because of the assertion above # Create the Vhost object ssl_vhost = self._create_vhost(vh_p) self.vhosts.append(ssl_vhost) # NOTE: Searches through Augeas seem to ruin changes to directives # The configuration must also be saved before being searched # for the new directives; For these reasons... this is tacked # on after fully creating the new vhost # Now check if addresses need to be added as NameBasedVhost addrs # This is for compliance with versions of Apache < 2.4 self._add_name_vhost_if_necessary(ssl_vhost) return ssl_vhost
def make_vhost_ssl(self, nonssl_vhost): # pylint: disable=too-many-locals """Makes an ssl_vhost version of a nonssl_vhost. Duplicates vhost and adds default ssl options New vhost will reside as (nonssl_vhost.path) + ``letsencrypt_apache.constants.CLI_DEFAULTS["le_vhost_ext"]`` .. note:: This function saves the configuration :param nonssl_vhost: Valid VH that doesn't have SSLEngine on :type nonssl_vhost: :class:`~letsencrypt_apache.obj.VirtualHost` :returns: SSL vhost :rtype: :class:`~letsencrypt_apache.obj.VirtualHost` :raises .errors.PluginError: If more than one virtual host is in the file or if plugin is unable to write/read vhost files. """ avail_fp = nonssl_vhost.filep # Get filepath of new ssl_vhost if avail_fp.endswith(".conf"): ssl_fp = avail_fp[:-(len(".conf"))] + self.conf("le_vhost_ext") else: ssl_fp = avail_fp + self.conf("le_vhost_ext") # First register the creation so that it is properly removed if # configuration is rolled back self.reverter.register_file_creation(False, ssl_fp) try: with open(avail_fp, "r") as orig_file: with open(ssl_fp, "w") as new_file: new_file.write("<IfModule mod_ssl.c>\n") for line in orig_file: new_file.write(line) new_file.write("</IfModule>\n") except IOError: logger.fatal("Error writing/reading to file in make_vhost_ssl") raise errors.PluginError("Unable to write/read in make_vhost_ssl") self.aug.load() ssl_addrs = set() # change address to address:443 addr_match = "/files%s//* [label()=~regexp('%s')]/arg" ssl_addr_p = self.aug.match(addr_match % (ssl_fp, parser.case_i("VirtualHost"))) for addr in ssl_addr_p: old_addr = common.Addr.fromstring(str(self.aug.get(addr))) ssl_addr = old_addr.get_addr_obj("443") self.aug.set(addr, str(ssl_addr)) ssl_addrs.add(ssl_addr) # Add directives vh_p = self.aug.match("/files%s//* [label()=~regexp('%s')]" % (ssl_fp, parser.case_i("VirtualHost"))) if len(vh_p) != 1: logger.error("Error: should only be one vhost in %s", avail_fp) raise errors.PluginError("Only one vhost per file is allowed") 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 logger.info("Created an SSL vhost at %s", ssl_fp) self.save_notes += "Created ssl vhost at %s\n" % ssl_fp self.save() # We know the length is one because of the assertion above ssl_vhost = self._create_vhost(vh_p[0]) self.vhosts.append(ssl_vhost) # NOTE: Searches through Augeas seem to ruin changes to directives # The configuration must also be saved before being searched # for the new directives; For these reasons... this is tacked # on after fully creating the new vhost need_to_save = False # See if the exact address appears in any other vhost for addr in ssl_addrs: for vhost in self.vhosts: if (ssl_vhost.filep != vhost.filep and addr in vhost.addrs and not self.is_name_vhost(addr)): self.add_name_vhost(addr) logger.info("Enabling NameVirtualHosts on %s", addr) need_to_save = True if need_to_save: self.save() return ssl_vhost
def make_vhost_ssl(self, nonssl_vhost): # pylint: disable=too-many-locals """Makes an ssl_vhost version of a nonssl_vhost. Duplicates vhost and adds default ssl options New vhost will reside as (nonssl_vhost.path) + ``letsencrypt_apache.constants.CLI_DEFAULTS["le_vhost_ext"]`` .. note:: This function saves the configuration :param nonssl_vhost: Valid VH that doesn't have SSLEngine on :type nonssl_vhost: :class:`~letsencrypt_apache.obj.VirtualHost` :returns: SSL vhost :rtype: :class:`~letsencrypt_apache.obj.VirtualHost` :raises .errors.PluginError: If more than one virtual host is in the file or if plugin is unable to write/read vhost files. """ avail_fp = nonssl_vhost.filep # Get filepath of new ssl_vhost if avail_fp.endswith(".conf"): ssl_fp = avail_fp[:-(len(".conf"))] + self.conf("le_vhost_ext") else: ssl_fp = avail_fp + self.conf("le_vhost_ext") # First register the creation so that it is properly removed if # configuration is rolled back self.reverter.register_file_creation(False, ssl_fp) try: with open(avail_fp, "r") as orig_file: with open(ssl_fp, "w") as new_file: new_file.write("<IfModule mod_ssl.c>\n") for line in orig_file: new_file.write(line) new_file.write("</IfModule>\n") except IOError: logger.fatal("Error writing/reading to file in make_vhost_ssl") raise errors.PluginError("Unable to write/read in make_vhost_ssl") self.aug.load() ssl_addrs = set() # change address to address:443 addr_match = "/files%s//* [label()=~regexp('%s')]/arg" ssl_addr_p = self.aug.match( addr_match % (ssl_fp, parser.case_i("VirtualHost"))) for addr in ssl_addr_p: old_addr = common.Addr.fromstring( str(self.aug.get(addr))) ssl_addr = old_addr.get_addr_obj("443") self.aug.set(addr, str(ssl_addr)) ssl_addrs.add(ssl_addr) # Add directives vh_p = self.aug.match("/files%s//* [label()=~regexp('%s')]" % (ssl_fp, parser.case_i("VirtualHost"))) if len(vh_p) != 1: logger.error("Error: should only be one vhost in %s", avail_fp) raise errors.PluginError("Only one vhost per file is allowed") 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 logger.info("Created an SSL vhost at %s", ssl_fp) self.save_notes += "Created ssl vhost at %s\n" % ssl_fp self.save() # We know the length is one because of the assertion above ssl_vhost = self._create_vhost(vh_p[0]) self.vhosts.append(ssl_vhost) # NOTE: Searches through Augeas seem to ruin changes to directives # The configuration must also be saved before being searched # for the new directives; For these reasons... this is tacked # on after fully creating the new vhost need_to_save = False # See if the exact address appears in any other vhost for addr in ssl_addrs: for vhost in self.vhosts: if (ssl_vhost.filep != vhost.filep and addr in vhost.addrs and not self.is_name_vhost(addr)): self.add_name_vhost(addr) logger.info("Enabling NameVirtualHosts on %s", addr) need_to_save = True if need_to_save: self.save() return ssl_vhost