From 46db59d7744065acc724485da3cf317e82c0e4cb Mon Sep 17 00:00:00 2001 From: yan Date: Mon, 23 Mar 2015 13:53:44 -0400 Subject: [PATCH 01/38] start adding nginx stubs --- letsencrypt/client/plugins/nginx/__init__.py | 1 + .../client/plugins/nginx/configurator.py | 1163 +++++++++++++++++ letsencrypt/client/plugins/nginx/dvsni.py | 201 +++ .../plugins/nginx/nginx_configurator.py | 208 +++ .../client/plugins/nginx/nginxparser.py | 110 ++ letsencrypt/client/plugins/nginx/obj.py | 91 ++ .../client/plugins/nginx/options-ssl.conf | 27 + letsencrypt/client/plugins/nginx/parser.py | 413 ++++++ .../client/plugins/nginx/tests/__init__.py | 1 + .../plugins/nginx/tests/configurator_test.py | 196 +++ .../client/plugins/nginx/tests/dvsni_test.py | 170 +++ .../plugins/nginx/tests/nginxparser_test.py | 101 ++ .../client/plugins/nginx/tests/obj_test.py | 68 + .../client/plugins/nginx/tests/parser_test.py | 129 ++ .../plugins/nginx/tests/testdata/foo.conf | 23 + .../plugins/nginx/tests/testdata/nginx.conf | 117 ++ .../nginx/tests/testdata/nginx.new.conf | 82 ++ .../client/plugins/nginx/tests/util.py | 112 ++ setup.py | 4 + 19 files changed, 3217 insertions(+) create mode 100644 letsencrypt/client/plugins/nginx/__init__.py create mode 100644 letsencrypt/client/plugins/nginx/configurator.py create mode 100644 letsencrypt/client/plugins/nginx/dvsni.py create mode 100644 letsencrypt/client/plugins/nginx/nginx_configurator.py create mode 100644 letsencrypt/client/plugins/nginx/nginxparser.py create mode 100644 letsencrypt/client/plugins/nginx/obj.py create mode 100644 letsencrypt/client/plugins/nginx/options-ssl.conf create mode 100644 letsencrypt/client/plugins/nginx/parser.py create mode 100644 letsencrypt/client/plugins/nginx/tests/__init__.py create mode 100644 letsencrypt/client/plugins/nginx/tests/configurator_test.py create mode 100644 letsencrypt/client/plugins/nginx/tests/dvsni_test.py create mode 100644 letsencrypt/client/plugins/nginx/tests/nginxparser_test.py create mode 100644 letsencrypt/client/plugins/nginx/tests/obj_test.py create mode 100644 letsencrypt/client/plugins/nginx/tests/parser_test.py create mode 100644 letsencrypt/client/plugins/nginx/tests/testdata/foo.conf create mode 100644 letsencrypt/client/plugins/nginx/tests/testdata/nginx.conf create mode 100644 letsencrypt/client/plugins/nginx/tests/testdata/nginx.new.conf create mode 100644 letsencrypt/client/plugins/nginx/tests/util.py diff --git a/letsencrypt/client/plugins/nginx/__init__.py b/letsencrypt/client/plugins/nginx/__init__.py new file mode 100644 index 000000000..63728924f --- /dev/null +++ b/letsencrypt/client/plugins/nginx/__init__.py @@ -0,0 +1 @@ +"""Let's Encrypt client.plugins.nginx.""" diff --git a/letsencrypt/client/plugins/nginx/configurator.py b/letsencrypt/client/plugins/nginx/configurator.py new file mode 100644 index 000000000..240dbe55e --- /dev/null +++ b/letsencrypt/client/plugins/nginx/configurator.py @@ -0,0 +1,1163 @@ +"""Nginx Configuration based off of Augeas Configurator.""" +import logging +import os +import re +import shutil +import socket +import subprocess +import sys + +import zope.interface + +from letsencrypt.acme import challenges + +from letsencrypt.client import achallenges +from letsencrypt.client import augeas_configurator +from letsencrypt.client import constants +from letsencrypt.client import errors +from letsencrypt.client import interfaces +from letsencrypt.client import le_util + +from letsencrypt.client.plugins.nginx import dvsni +from letsencrypt.client.plugins.nginx import obj +from letsencrypt.client.plugins.nginx import parser + + +# TODO: Augeas sections ie. , beginning and closing +# tags need to be the same case, otherwise Augeas doesn't recognize them. +# This is not able to be completely remedied by regular expressions because +# 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 LE's transactions will ensure +# only properly formed sections are added. + +# Note: This protocol works for filenames with spaces in it, the sites are +# properly set up and directives are changed appropriately, but Nginx won't +# recognize names in sites-enabled that have spaces. These are not added to the +# Nginx configuration. It may be wise to warn the user if they are trying +# to use vhost filenames that contain spaces and offer to change ' ' to '_' + +# Note: FILEPATHS and changes to files are transactional. They are copied +# over before the updates are made to the existing files. NEW_FILES is +# transactional due to the use of register_file_creation() + + +class NginxConfigurator(augeas_configurator.AugeasConfigurator): + # pylint: disable=too-many-instance-attributes,too-many-public-methods + """Nginx configurator. + + State of Configurator: This code has been tested under Ubuntu 12.04 + Nginx 2.2 and this code works for Ubuntu 14.04 Nginx 2.4. Further + notes below. + + This class was originally developed for Nginx 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. + + .. 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 clients are clarified with the new and developing protocol. + + :ivar config: Configuration. + :type config: :class:`~letsencrypt.client.interfaces.IConfig` + + :ivar parser: Handles low level parsing + :type parser: :class:`~letsencrypt.client.plugins.nginx.parser` + + :ivar tup version: version of Nginx + :ivar list vhosts: All vhosts found in the configuration + (:class:`list` of + :class:`~letsencrypt.client.plugins.nginx.obj.VirtualHost`) + + :ivar dict assoc: Mapping between domains and vhosts + + """ + zope.interface.implements(interfaces.IAuthenticator, interfaces.IInstaller) + + description = "Nginx Web Server" + + def __init__(self, config, version=None): + """Initialize an Nginx Configurator. + + :param tup version: version of Nginx as a tuple (2, 4, 7) + (used mostly for unittesting) + + """ + super(NginxConfigurator, self).__init__(config) + + # Verify that all directories and files exist with proper permissions + if os.geteuid() == 0: + self.verify_setup() + + # Add name_server association dict + self.assoc = dict() + # Add number of outstanding challenges + self._chall_out = 0 + + # These will be set in the prepare function + self.parser = None + self.version = version + self.vhosts = None + self._enhance_func = {"redirect": self._enable_redirect} + + def prepare(self): + """Prepare the authenticator/installer.""" + self.parser = parser.NginxParser( + self.aug, self.config.nginx_server_root, + self.config.nginx_mod_ssl_conf) + # Check for errors in parsing files with Augeas + self.check_parsing_errors("httpd.aug") + + # Set Version + if self.version is None: + self.version = self.get_version() + + # Get all of the available vhosts + self.vhosts = self.get_virtual_hosts() + + # Enable mod_ssl if it isn't already enabled + # This is Let's Encrypt... we enable mod_ssl on initialization :) + # TODO: attempt to make the check faster... this enable should + # be asynchronous as it shouldn't be that time sensitive + # on initialization + self._prepare_server_https() + + temp_install(self.config.nginx_mod_ssl_conf) + + def deploy_cert(self, domain, cert, key, cert_chain=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 + + :param str domain: domain to deploy certificate + :param str cert: certificate filename + :param str key: private key filename + :param str cert_chain: certificate chain filename + + """ + vhost = self.choose_vhost(domain) + path = {} + + path["cert_file"] = 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 cert_chain is not None: + 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" + logging.warn( + "Cannot find a cert or key directive in %s", vhost.path) + logging.warn("VirtualHost was not modified") + # Presumably break here so that the virtualhost is not modified + return False + + logging.info("Deploying Certificate to VirtualHost %s", vhost.filep) + + self.aug.set(path["cert_file"][0], cert) + self.aug.set(path["cert_key"][0], key) + if cert_chain is not None: + if len(path["cert_chain"]) == 0: + self.parser.add_dir( + vhost.path, "SSLCertificateChainFile", cert_chain) + else: + self.aug.set(path["cert_chain"][0], cert_chain) + + 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 + self.save_notes += "\tSSLCertificateKeyFile %s\n" % key + if cert_chain: + self.save_notes += "\tSSLCertificateChainFile %s\n" % cert_chain + + # Make sure vhost is enabled + if not vhost.enabled: + self.enable_site(vhost) + + def choose_vhost(self, target_name): + """Chooses a virtual host based on the given domain name. + + .. todo:: This should maybe return list if no obvious answer + is presented. + + :param str target_name: domain name + + :returns: ssl vhost associated with name + :rtype: :class:`~letsencrypt.client.plugins.nginx.obj.VirtualHost` + + """ + # Allows for domain names to be associated with a virtual host + # Client isn't using create_dn_server_assoc(self, dn, vh) yet + if target_name in self.assoc: + return self.assoc[target_name] + # Check for servernames/aliases for ssl hosts + for vhost in self.vhosts: + if vhost.ssl and target_name in vhost.names: + self.assoc[target_name] = vhost + return vhost + # Checking for domain name in vhost address + # This technique is not recommended by Nginx but is technically valid + target_addr = obj.Addr((target_name, "443")) + for vhost in self.vhosts: + if target_addr in vhost.addrs: + self.assoc[target_name] = vhost + return vhost + + # Check for non ssl vhosts with servernames/aliases == "name" + for vhost in self.vhosts: + if not vhost.ssl and target_name in vhost.names: + vhost = self.make_vhost_ssl(vhost) + self.assoc[target_name] = vhost + return vhost + + # No matches, search for the default + for vhost in self.vhosts: + if "_default_:443" in vhost.addrs: + return vhost + return None + + def create_dn_server_assoc(self, domain, vhost): + """Create an association between a domain name and virtual host. + + Helps to choose an appropriate vhost + + :param str domain: domain name to associate + + :param vhost: virtual host to associate with domain + :type vhost: :class:`~letsencrypt.client.plugins.nginx.obj.VirtualHost` + + """ + self.assoc[domain] = vhost + + def get_all_names(self): + """Returns all names found in the Nginx Configuration. + + :returns: All ServerNames, ServerAliases, and reverse DNS entries for + virtual host addresses + :rtype: set + + """ + all_names = set() + + # Kept in same function to avoid multiple compilations of the regex + priv_ip_regex = (r"(^127\.0\.0\.1)|(^10\.)|(^172\.1[6-9]\.)|" + r"(^172\.2[0-9]\.)|(^172\.3[0-1]\.)|(^192\.168\.)") + private_ips = re.compile(priv_ip_regex) + + for vhost in self.vhosts: + all_names.update(vhost.names) + for addr in vhost.addrs: + # If it isn't a private IP, do a reverse DNS lookup + if not private_ips.match(addr.get_addr()): + try: + socket.inet_aton(addr.get_addr()) + all_names.add(socket.gethostbyaddr(addr.get_addr())[0]) + except (socket.error, socket.herror, socket.timeout): + continue + + return all_names + + 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.plugins.nginx.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 _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.client.plugins.nginx.obj.VirtualHost` + + """ + addrs = set() + args = self.aug.match(path + "/arg") + for arg in args: + addrs.add(obj.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 + + # TODO: make "sites-available" a configurable directory + def get_virtual_hosts(self): + """Returns list of virtual hosts found in the Nginx configuration. + + :returns: List of + :class:`~letsencrypt.client.plugins.nginx.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 is_name_vhost(self, target_addr): + r"""Returns if vhost is a name based vhost + + NameVirtualHost was deprecated in Nginx 2.4 as all VirtualHosts are + now NameVirtualHosts. If version is earlier than 2.4, check if addr + has a NameVirtualHost directive in the Nginx 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 Nginx 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 add_name_vhost(self, addr): + """Adds NameVirtualHost directive for given address. + + :param str addr: Address that will be added as NameVirtualHost directive + + """ + 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 _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 mod_loaded("ssl_module", self.config.nginx_ctl): + logging.info("Loading mod_ssl into Nginx Server") + enable_mod("ssl", self.config.nginx_init_script, + self.config.nginx_enmod) + + # 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: + logging.debug("No Listen 443 directive found") + logging.debug("Setting the Nginx 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_server_sni_ready(self, vhost, default_addr="*:443"): + """Checks to see if the server is ready for SNI challenges. + + :param vhost: VirtualHost to check SNI compatibility + :type vhost: :class:`~letsencrypt.client.plugins.nginx.obj.VirtualHost` + + :param str default_addr: TODO - investigate function further + + """ + if self.version >= (2, 4): + return + # Check for NameVirtualHost + # First see if any of the vhost addresses is a _default_ addr + for addr in vhost.addrs: + if addr.get_addr() == "_default_": + if not self.is_name_vhost(default_addr): + logging.debug("Setting all VirtualHosts on %s to be " + "name based vhosts", default_addr) + self.add_name_vhost(default_addr) + + # No default addresses... so set each one individually + for addr in vhost.addrs: + if not self.is_name_vhost(addr): + logging.debug("Setting VirtualHost at %s to be a name " + "based virtual host", addr) + self.add_name_vhost(addr) + + 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) + ``IConfig.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.client.plugins.nginx.obj.VirtualHost` + + :returns: SSL vhost + :rtype: :class:`~letsencrypt.client.plugins.nginx.obj.VirtualHost` + + """ + avail_fp = nonssl_vhost.filep + # Get filepath of new ssl_vhost + if avail_fp.endswith(".conf"): + ssl_fp = avail_fp[:-(len(".conf"))] + self.config.le_vhost_ext + else: + ssl_fp = avail_fp + self.config.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("\n") + for line in orig_file: + new_file.write(line) + new_file.write("\n") + except IOError: + logging.fatal("Error writing/reading to file in make_vhost_ssl") + sys.exit(49) + + 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 = obj.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: + logging.error("Error: should only be one vhost in %s", avail_fp) + sys.exit(1) + + 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) + 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) + logging.info("Enabling NameVirtualHosts on %s", addr) + need_to_save = True + + if need_to_save: + self.save() + + return ssl_vhost + + def supported_enhancements(self): # pylint: disable=no-self-use + """Returns currently supported enhancements.""" + return ["redirect"] + + def enhance(self, domain, enhancement, options=None): + """Enhance configuration. + + :param str domain: domain to enhance + :param str enhancement: enhancement type defined in + :const:`~letsencrypt.client.constants.ENHANCEMENTS` + :param options: options for the enhancement + See :const:`~letsencrypt.client.constants.ENHANCEMENTS` + documentation for appropriate parameter. + + """ + try: + return self._enhance_func[enhancement]( + self.choose_vhost(domain), options) + except ValueError: + raise errors.LetsEncryptConfiguratorError( + "Unsupported enhancement: {}".format(enhancement)) + except errors.LetsEncryptConfiguratorError: + logging.warn("Failed %s for %s", enhancement, domain) + + def _enable_redirect(self, ssl_vhost, unused_options): + """Redirect all equivalent HTTP traffic to ssl_vhost. + + .. todo:: This enhancement should be rewritten and will + unfortunately require lots of debugging by hand. + + Adds Redirect directive to the port 80 equivalent of ssl_vhost + First the function attempts to find the vhost with equivalent + ip addresses that serves on non-ssl ports + The function then adds the directive + + .. note:: This function saves the configuration + + :param ssl_vhost: Destination of traffic, an ssl enabled vhost + :type ssl_vhost: + :class:`~letsencrypt.client.plugins.nginx.obj.VirtualHost` + + :param unused_options: Not currently used + :type unused_options: Not Available + + :returns: Success, general_vhost (HTTP vhost) + :rtype: (bool, + :class:`~letsencrypt.client.plugins.nginx.obj.VirtualHost`) + + """ + if not mod_loaded("rewrite_module", self.config.nginx_ctl): + enable_mod("rewrite", self.config.nginx_init_script, + self.config.nginx_enmod) + + general_v = self._general_vhost(ssl_vhost) + if general_v is None: + # Add virtual_server with redirect + logging.debug( + "Did not find http version of ssl virtual host... creating") + return self._create_redirect_vhost(ssl_vhost) + else: + # Check if redirection already exists + exists, code = self._existing_redirect(general_v) + if exists: + if code == 0: + logging.debug("Redirect already added") + logging.info( + "Configuration is already redirecting traffic to HTTPS") + return + else: + logging.info("Unknown redirect exists for this vhost") + raise errors.LetsEncryptConfiguratorError( + "Unknown redirect already exists " + "in {}".format(general_v.filep)) + # Add directives to server + self.parser.add_dir(general_v.path, "RewriteEngine", "On") + self.parser.add_dir(general_v.path, "RewriteRule", + constants.APACHE_REWRITE_HTTPS_ARGS) + self.save_notes += ("Redirecting host in %s to ssl vhost in %s\n" % + (general_v.filep, ssl_vhost.filep)) + self.save() + + logging.info("Redirecting vhost in %s to ssl vhost in %s", + general_v.filep, ssl_vhost.filep) + + 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.client.plugins.nginx.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.APACHE_REWRITE_HTTPS_ARGS): + for idx, match in enumerate(rewrite_path): + if (self.aug.get(match) != + constants.APACHE_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 _create_redirect_vhost(self, ssl_vhost): + """Creates an http_vhost specifically to redirect for the ssl_vhost. + + :param ssl_vhost: ssl vhost + :type ssl_vhost: + :class:`~letsencrypt.client.plugins.nginx.obj.VirtualHost` + + :returns: tuple of the form + (`success`, + :class:`~letsencrypt.client.plugins.nginx.obj.VirtualHost`) + :rtype: tuple + + """ + # Consider changing this to a dictionary check + # Make sure adding the vhost will be safe + conflict, host_or_addrs = self._conflicting_host(ssl_vhost) + if conflict: + raise errors.LetsEncryptConfiguratorError( + "Unable to create a redirection vhost " + "- {}".format(host_or_addrs)) + + redirect_addrs = host_or_addrs + + # get servernames and serveraliases + serveralias = "" + servername = "" + size_n = len(ssl_vhost.names) + if size_n > 0: + servername = "ServerName " + ssl_vhost.names[0] + if size_n > 1: + serveralias = " ".join(ssl_vhost.names[1:size_n]) + serveralias = "ServerAlias " + serveralias + redirect_file = ("\n" + "%s \n" + "%s \n" + "ServerSignature Off\n" + "\n" + "RewriteEngine On\n" + "RewriteRule %s\n" + "\n" + "ErrorLog /var/log/nginx2/redirect.error.log\n" + "LogLevel warn\n" + "\n" + % (servername, serveralias, + " ".join(constants.APACHE_REWRITE_HTTPS_ARGS))) + + # Write out the file + # This is the default name + redirect_filename = "le-redirect.conf" + + # See if a more appropriate name can be applied + if len(ssl_vhost.names) > 0: + # Sanity check... + # make sure servername doesn't exceed filename length restriction + if ssl_vhost.names[0] < (255-23): + redirect_filename = "le-redirect-%s.conf" % ssl_vhost.names[0] + + redirect_filepath = os.path.join( + self.parser.root, "sites-available", redirect_filename) + + # Register the new file that will be created + # Note: always register the creation before writing to ensure file will + # be removed in case of unexpected program exit + self.reverter.register_file_creation(False, redirect_filepath) + + # Write out file + with open(redirect_filepath, "w") as redirect_fd: + redirect_fd.write(redirect_file) + logging.info("Created redirect file: %s", redirect_filename) + + self.aug.load() + # Make a new vhost data structure and add it to the lists + new_vhost = self._create_vhost(parser.get_aug_path(redirect_filepath)) + self.vhosts.append(new_vhost) + + # Finally create documentation for the change + self.save_notes += ("Created a port 80 vhost, %s, for redirection to " + "ssl vhost %s\n" % + (new_vhost.filep, ssl_vhost.filep)) + + def _conflicting_host(self, ssl_vhost): + """Checks for conflicting HTTP vhost for ssl_vhost. + + Checks for a conflicting host, such that a new port 80 host could not + be created without ruining the nginx config + Used with redirection + + returns: conflict, host_or_addrs - boolean + if conflict: returns conflicting vhost + 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.plugins.nginx.obj.VirtualHost` + + :returns: TODO + :rtype: TODO + + """ + # Consider changing this to a dictionary check + redirect_addrs = "" + for ssl_a in ssl_vhost.addrs: + # Add space on each new addr, combine "VirtualHost"+redirect_addrs + redirect_addrs = redirect_addrs + " " + ssl_a_vhttp = ssl_a.get_addr_obj("80") + # Search for a conflicting host... + for vhost in self.vhosts: + if vhost.enabled: + if (ssl_a_vhttp in vhost.addrs or + ssl_a.get_addr_obj("") in vhost.addrs or + ssl_a.get_addr_obj("*") in vhost.addrs): + # We have found a conflicting host... just return + return True, vhost + + redirect_addrs = redirect_addrs + ssl_a_vhttp + + return False, redirect_addrs + + def _general_vhost(self, ssl_vhost): + """Find appropriate HTTP vhost for ssl_vhost. + + Function needs to be thoroughly tested and perhaps improved + Will not do well with malformed configurations + Consider changing this into a dict check + + :param ssl_vhost: ssl vhost to check + :type ssl_vhost: + :class:`~letsencrypt.client.plugins.nginx.obj.VirtualHost` + + :returns: HTTP vhost or None if unsuccessful + :rtype: :class:`~letsencrypt.client.plugins.nginx.obj.VirtualHost` + or None + + """ + # _default_:443 check + # Instead... should look for vhost of the form *:80 + # Should we prompt the user? + ssl_addrs = ssl_vhost.addrs + if ssl_addrs == obj.Addr.fromstring("_default_:443"): + ssl_addrs = [obj.Addr.fromstring("*:443")] + + for vhost in self.vhosts: + found = 0 + # Not the same vhost, and same number of addresses + if vhost != ssl_vhost and len(vhost.addrs) == len(ssl_vhost.addrs): + # Find each address in ssl_host in test_host + for ssl_a in ssl_addrs: + for test_a in vhost.addrs: + if test_a.get_addr() == ssl_a.get_addr(): + # Check if found... + if (test_a.get_port() == "80" or + test_a.get_port() == "" or + test_a.get_port() == "*"): + found += 1 + break + # Check to make sure all addresses were found + # and names are equal + if (found == len(ssl_vhost.addrs) and + vhost.names == ssl_vhost.names): + return vhost + return None + + def get_all_certs_keys(self): + """Find all existing keys, certs from configuration. + + Retrieve all certs and keys set in VirtualHosts on the Nginx 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: + logging.error("Too many cert or key directives in vhost %s", + vhost.filep) + sys.exit(40) + + 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 is_site_enabled(self, avail_fp): + """Checks to see if the given site is enabled. + + .. todo:: fix hardcoded sites-enabled, check os.path.samefile + + :param str avail_fp: Complete file path of available site + + :returns: Success + :rtype: bool + + """ + enabled_dir = os.path.join(self.parser.root, "sites-enabled") + for entry in os.listdir(enabled_dir): + if os.path.realpath(os.path.join(enabled_dir, entry)) == avail_fp: + return True + + return False + + def enable_site(self, vhost): + """Enables an available site, Nginx restart required. + + .. todo:: This function should number subdomains before the domain vhost + + .. todo:: Make sure link is not broken... + + :param vhost: vhost to enable + :type vhost: :class:`~letsencrypt.client.plugins.nginx.obj.VirtualHost` + + :returns: Success + :rtype: bool + + """ + if self.is_site_enabled(vhost.filep): + return True + + if "/sites-available/" in vhost.filep: + enabled_path = ("%s/sites-enabled/%s" % + (self.parser.root, os.path.basename(vhost.filep))) + self.reverter.register_file_creation(False, enabled_path) + os.symlink(vhost.filep, enabled_path) + vhost.enabled = True + logging.info("Enabling available site: %s", vhost.filep) + self.save_notes += "Enabled site %s\n" % vhost.filep + return True + return False + + def restart(self): + """Restarts nginx server. + + :returns: Success + :rtype: bool + + """ + return nginx_restart(self.config.nginx_init_script) + + def config_test(self): # pylint: disable=no-self-use + """Check the configuration of Nginx for errors. + + :returns: Success + :rtype: bool + + """ + try: + proc = subprocess.Popen( + ["sudo", self.config.nginx_ctl, "configtest"], # TODO: sudo? + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + stdout, stderr = proc.communicate() + except (OSError, ValueError): + logging.fatal("Unable to run /usr/sbin/nginx2ctl configtest") + sys.exit(1) + + if proc.returncode != 0: + # Enter recovery routine... + logging.error("Configtest failed") + logging.error(stdout) + logging.error(stderr) + return False + + return True + + def verify_setup(self): + """Verify the setup to ensure safe operating environment. + + Make sure that files/directories are setup with appropriate permissions + Aim for defensive coding... make sure all input files + have permissions of root + + """ + uid = os.geteuid() + le_util.make_or_verify_dir(self.config.config_dir, 0o755, uid) + le_util.make_or_verify_dir(self.config.work_dir, 0o755, uid) + le_util.make_or_verify_dir(self.config.backup_dir, 0o755, uid) + + def get_version(self): + """Return version of Nginx Server. + + Version is returned as tuple. (ie. 2.4.7 = (2, 4, 7)) + + :returns: version + :rtype: tuple + + :raises errors.LetsEncryptConfiguratorError: + Unable to find Nginx version + + """ + try: + proc = subprocess.Popen( + [self.config.nginx_ctl, "-v"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + text = proc.communicate()[0] + except (OSError, ValueError): + raise errors.LetsEncryptConfiguratorError( + "Unable to run %s -v" % self.config.nginx_ctl) + + regex = re.compile(r"Nginx/([0-9\.]*)", re.IGNORECASE) + matches = regex.findall(text) + + if len(matches) != 1: + raise errors.LetsEncryptConfiguratorError( + "Unable to find Nginx version") + + return tuple([int(i) for i in matches[0].split(".")]) + + def more_info(self): + """Human-readable string to help understand the module""" + return ( + "Configures Nginx to authenticate and install HTTPS.{0}" + "Server root: {root}{0}" + "Version: {version}".format( + os.linesep, root=self.parser.loc["root"], + version=".".join(str(i) for i in self.version)) + ) + + ########################################################################### + # Challenges Section + ########################################################################### + def get_chall_pref(self, unused_domain): # pylint: disable=no-self-use + """Return list of challenge preferences.""" + return [challenges.DVSNI] + + def perform(self, achalls): + """Perform the configuration related challenge. + + This function currently assumes all challenges will be fulfilled. + If this turns out not to be the case in the future. Cleanup and + outstanding challenges will have to be designed better. + + """ + self._chall_out += len(achalls) + responses = [None] * len(achalls) + nginx_dvsni = dvsni.NginxDvsni(self) + + for i, achall in enumerate(achalls): + if isinstance(achall, achallenges.DVSNI): + # Currently also have dvsni hold associated index + # of the challenge. This helps to put all of the responses back + # together when they are all complete. + nginx_dvsni.add_chall(achall, i) + + sni_response = nginx_dvsni.perform() + # Must restart in order to activate the challenges. + # Handled here because we may be able to load up other challenge types + self.restart() + + # Go through all of the challenges and assign them to the proper place + # in the responses return value. All responses must be in the same order + # as the original challenges. + for i, resp in enumerate(sni_response): + responses[nginx_dvsni.indices[i]] = resp + + return responses + + def cleanup(self, achalls): + """Revert all challenges.""" + self._chall_out -= len(achalls) + + # If all of the challenges have been finished, clean up everything + if self._chall_out <= 0: + self.revert_challenge_config() + self.restart() + + +def enable_mod(mod_name, nginx_init_script, nginx_enmod): + """Enables module in Nginx. + + Both enables and restarts Nginx so module is active. + + :param str mod_name: Name of the module to enable. + :param str nginx_init_script: Path to the Nginx init script. + :param str nginx_enmod: Path to the Nginx a2enmod script. + + """ + try: + # Use check_output so the command will finish before reloading + # TODO: a2enmod is debian specific... + subprocess.check_call(["sudo", nginx_enmod, mod_name], # TODO: sudo? + stdout=open("/dev/null", "w"), + stderr=open("/dev/null", "w")) + nginx_restart(nginx_init_script) + except (OSError, subprocess.CalledProcessError) as err: + logging.error("Error enabling mod_%s", mod_name) + logging.error("Exception: %s", err) + sys.exit(1) + + +def mod_loaded(module, nginx_ctl): + """Checks to see if mod_ssl is loaded + + Uses ``nginx_ctl`` to get loaded module list. This also effectively + serves as a config_test. + + :param str nginx_ctl: Path to nginx2ctl binary. + + :returns: If ssl_module is included and active in Nginx + :rtype: bool + + """ + try: + proc = subprocess.Popen( + [nginx_ctl, "-M"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + stdout, stderr = proc.communicate() + + except (OSError, ValueError): + logging.error( + "Error accessing %s for loaded modules!", nginx_ctl) + raise errors.LetsEncryptConfiguratorError( + "Error accessing loaded modules") + # Small errors that do not impede + if proc.returncode != 0: + logging.warn("Error in checking loaded module list: %s", stderr) + raise errors.LetsEncryptMisconfigurationError( + "Nginx is unable to check whether or not the module is " + "loaded because Nginx is misconfigured.") + + if module in stdout: + return True + return False + + +def nginx_restart(nginx_init_script): + """Restarts the Nginx Server. + + :param str nginx_init_script: Path to the Nginx init script. + + .. todo:: Try to use reload instead. (This caused timing problems before) + + .. todo:: On failure, this should be a recovery_routine call with another + restart. This will confuse and inhibit developers from testing code + though. This change should happen after + the NginxConfigurator has been thoroughly tested. The function will + need to be moved into the class again. Perhaps + this version can live on... for testing purposes. + + """ + try: + proc = subprocess.Popen([nginx_init_script, "restart"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + stdout, stderr = proc.communicate() + + if proc.returncode != 0: + # Enter recovery routine... + logging.error("Nginx Restart Failed!") + logging.error(stdout) + logging.error(stderr) + return False + + except (OSError, ValueError): + logging.fatal( + "Nginx Restart Failed - Please Check the Configuration") + sys.exit(1) + + return True + + +def get_file_path(vhost_path): + """Get file path from augeas_vhost_path. + + Takes in Augeas path and returns the file name + + :param str vhost_path: Augeas virtual host path + + :returns: filename of vhost + :rtype: str + + """ + # Strip off /files + avail_fp = vhost_path[6:] + # This can be optimized... + while True: + # Cast both to lowercase to be case insensitive + find_if = avail_fp.lower().find("/ifmodule") + if find_if != -1: + avail_fp = avail_fp[:find_if] + continue + find_vh = avail_fp.lower().find("/virtualhost") + if find_vh != -1: + avail_fp = avail_fp[:find_vh] + continue + break + return avail_fp + + +def temp_install(options_ssl): + """Temporary install for convenience.""" + # WARNING: THIS IS A POTENTIAL SECURITY VULNERABILITY + # THIS SHOULD BE HANDLED BY THE PACKAGE MANAGER + # AND TAKEN OUT BEFORE RELEASE, INSTEAD + # SHOWING A NICE ERROR MESSAGE ABOUT THE PROBLEM. + + # Check to make sure options-ssl.conf is installed + if not os.path.isfile(options_ssl): + shutil.copyfile(constants.APACHE_MOD_SSL_CONF, options_ssl) diff --git a/letsencrypt/client/plugins/nginx/dvsni.py b/letsencrypt/client/plugins/nginx/dvsni.py new file mode 100644 index 000000000..960352831 --- /dev/null +++ b/letsencrypt/client/plugins/nginx/dvsni.py @@ -0,0 +1,201 @@ +"""NginxDVSNI""" +import logging +import os + +from letsencrypt.client.plugins.nginx import parser + + +class NginxDvsni(object): + """Class performs DVSNI challenges within the Nginx configurator. + + :ivar configurator: NginxConfigurator object + :type configurator: :class:`~nginx.configurator.NginxConfigurator` + + :ivar list achalls: Annotated :class:`~letsencrypt.client.achallenges.DVSNI` + challenges. + + :param list indices: Meant to hold indices of challenges in a + larger array. NginxDvsni is capable of solving many challenges + at once which causes an indexing issue within NginxConfigurator + who must return all responses in order. Imagine NginxConfigurator + maintaining state about where all of the SimpleHTTPS Challenges, + Dvsni Challenges belong in the response array. This is an optional + utility. + + :param str challenge_conf: location of the challenge config file + + """ + + VHOST_TEMPLATE = """\ + + ServerName {server_name} + UseCanonicalName on + SSLStrictSNIVHostCheck on + + LimitRequestBody 1048576 + + Include {ssl_options_conf_path} + SSLCertificateFile {cert_path} + SSLCertificateKeyFile {key_path} + + DocumentRoot {document_root} + + +""" + def __init__(self, configurator): + self.configurator = configurator + self.achalls = [] + self.indices = [] + self.challenge_conf = os.path.join( + configurator.config.config_dir, "le_dvsni_cert_challenge.conf") + # self.completed = 0 + + def add_chall(self, achall, idx=None): + """Add challenge to DVSNI object to perform at once. + + :param achall: Annotated DVSNI challenge. + :type achall: :class:`letsencrypt.client.achallenges.DVSNI` + + :param int idx: index to challenge in a larger array + + """ + self.achalls.append(achall) + if idx is not None: + self.indices.append(idx) + + def perform(self): + """Peform a DVSNI challenge.""" + if not self.achalls: + return [] + # Save any changes to the configuration as a precaution + # 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) + if vhost is None: + logging.error( + "No vhost exists with servername or alias of: %s", + achall.domain) + logging.error("No _default_:443 vhost exists") + logging.error("Please specify servernames in the Nginx config") + return None + + # 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 + for achall in self.achalls: + responses.append(self._setup_challenge_cert(achall)) + + # Setup the configuration + self._mod_config(addresses) + + # Save reversible changes + self.configurator.save("SNI Challenge", True) + + return responses + + def _setup_challenge_cert(self, achall, s=None): + # pylint: disable=invalid-name + """Generate and write out challenge certificate.""" + cert_path = self.get_cert_file(achall) + # Register the path before you write out the file + self.configurator.reverter.register_file_creation(True, cert_path) + + cert_pem, response = achall.gen_cert_and_response(s) + + # Write out challenge cert + with open(cert_path, "w") as cert_chall_fd: + cert_chall_fd.write(cert_pem) + + return response + + def _mod_config(self, ll_addrs): + """Modifies Nginx config files to include challenge vhosts. + + Result: Nginx config includes virtual servers for issued challs + + :param list ll_addrs: list of list of + :class:`letsencrypt.client.plugins.nginx.obj.Addr` to apply + + """ + # TODO: Use ip address of existing vhost instead of relying on FQDN + config_text = "\n" + for idx, lis in enumerate(ll_addrs): + config_text += self._get_config_text(self.achalls[idx], lis) + config_text += "\n" + + self._conf_include_check(self.configurator.parser.loc["default"]) + self.configurator.reverter.register_file_creation( + True, self.challenge_conf) + + with open(self.challenge_conf, "w") as new_conf: + new_conf.write(config_text) + + def _conf_include_check(self, main_config): + """Adds DVSNI challenge conf file into configuration. + + Adds DVSNI challenge include file if it does not already exist + within mainConfig + + :param str main_config: file path to main user nginx 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_config_text(self, achall, ip_addrs): + """Chocolate virtual server configuration text + + :param achall: Annotated DVSNI challenge. + :type achall: :class:`letsencrypt.client.achallenges.DVSNI` + + :param list ip_addrs: addresses of challenged domain + :class:`list` of type :class:`~nginx.obj.Addr` + + :returns: virtual host configuration text + :rtype: str + + """ + ips = " ".join(str(i) for i in ip_addrs) + document_root = os.path.join( + self.configurator.config.config_dir, "dvsni_page/") + # TODO: Python docs is not clear how mutliline string literal + # newlines are parsed on different platforms. At least on + # Linux (Debian sid), when source file uses CRLF, Python still + # parses it as "\n"... c.f.: + # https://docs.python.org/2.7/reference/lexical_analysis.html + return self.VHOST_TEMPLATE.format( + vhost=ips, server_name=achall.nonce_domain, + ssl_options_conf_path=self.configurator.parser.loc["ssl_options"], + cert_path=self.get_cert_file(achall), key_path=achall.key.file, + document_root=document_root).replace("\n", os.linesep) + + def get_cert_file(self, achall): + """Returns standardized name for challenge certificate. + + :param achall: Annotated DVSNI challenge. + :type achall: :class:`letsencrypt.client.achallenges.DVSNI` + + :returns: certificate file name + :rtype: str + + """ + return os.path.join( + self.configurator.config.work_dir, achall.nonce_domain + ".crt") diff --git a/letsencrypt/client/plugins/nginx/nginx_configurator.py b/letsencrypt/client/plugins/nginx/nginx_configurator.py new file mode 100644 index 000000000..86aa7e371 --- /dev/null +++ b/letsencrypt/client/plugins/nginx/nginx_configurator.py @@ -0,0 +1,208 @@ +import zope.interface + +from letsencrypt.client import augeas_configurator +from letsencrypt.client import CONFIG +from letsencrypt.client import interfaces + + +# This might be helpful... but feel free to use whatever you want +# class VH(object): +# def __init__(self, filename_path, vh_path, vh_addrs, is_ssl, is_enabled): +# self.file = filename_path +# self.path = vh_path +# self.addrs = vh_addrs +# self.names = [] +# self.ssl = is_ssl +# self.enabled = is_enabled + +# def set_names(self, listOfNames): +# self.names = listOfNames + +# def add_name(self, name): +# self.names.append(name) + +class NginxConfigurator(augeas_configurator.AugeasConfigurator): + """Nginx Configurator class.""" + zope.interface.implements(interfaces.IAuthenticator, interfaces.IInstaller) + + def __init__(self, server_root=CONFIG.SERVER_ROOT): + super(NginxConfigurator, self).__init__() + 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 + # vhosts + self.recovery_routine() + # Check for errors in parsing files with Augeas + # TODO - insert nginx lens info here??? + #self.check_parsing_errors("httpd.aug") + + def deploy_cert(self, vhost, cert, key, cert_chain=None): + """Deploy cert in nginx""" + + def choose_virtual_host(self, name): + """Chooses a virtual host based on the given domain name""" + + def get_all_names(self): + """Returns all names found in the nginx configuration""" + return set() + + # Might be helpful... I know nothing about nginx lens + # def get_include_path(self, cur_dir, arg): + # """ + # Converts an Nginx Include directive argument into an Augeas + # searchable path + # Returns path string + # """ + # # 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 Nginx 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 nginx config doesn't work because it + # # would create a race condition between the check and this input + + # # TODO: Fix this + # # 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 Nginx + # elif arg.startswith("conf/"): + # arg = self.server_root + arg[5:] + # # TODO: Test if Nginx 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: + # postfix = "" + # splitArg = arg.split("/") + # for idx, split in enumerate(splitArg): + # # * and ? are the two special fnmatch characters + # if "*" in split or "?" in split: + # # Turn it into a augeas regex + # # TODO: Can this be an augeas glob instead of regex + # splitArg[idx] = ("* [label()=~regexp('%s')]" % + # self.fnmatch_to_re(split) + # # Reassemble the argument + # arg = "/".join(splitArg) + + # # If the include is a directory, just return the directory as a file + # if arg.endswith("/"): + # return "/files" + arg[:len(arg)-1] + # return "/files"+arg + + def enable_redirect(self, ssl_vhost): + """ + Adds Redirect directive to the port 80 equivalent of ssl_vhost + First the function attempts to find the vhost with equivalent + ip addresses that serves on non-ssl ports + The function then adds the directive + """ + return + + def enable_ocsp_stapling(self, ssl_vhost): + return False + + def enable_hsts(self, ssl_vhost): + return False + + def get_all_certs_keys(self): + """ + Retrieve all certs and keys set in VirtualHosts on the Nginx server + returns: list of tuples with form [(cert, key, path)] + """ + return None + + # Probably helpful reference + # def get_file_path(self, vhost_path): + # """ + # Takes in Augeas path and returns the file name + # """ + # # Strip off /files + # avail_fp = vhost_path[6:] + # # This can be optimized... + # while True: + # # Cast both to lowercase to be case insensitive + # find_if = avail_fp.lower().find("/ifmodule") + # if find_if != -1: + # avail_fp = avail_fp[:find_if] + # continue + # find_vh = avail_fp.lower().find("/virtualhost") + # if find_vh != -1: + # avail_fp = avail_fp[:find_vh] + # continue + # break + # return avail_fp + + def enable_site(self, vhost): + """Enables an available site, Nginx restart required""" + return False + + # Might be a usefule reference + # def parse_file(self, file_path): + # """ + # Checks to see if file_path is parsed by Augeas + # If file_path isn't parsed, the file is added and Augeas is reloaded + # """ + # # Test if augeas included file for Httpd.lens + # # Note: This works for augeas globs, ie. *.conf + # incTest = self.aug.match( + # "/augeas/load/Httpd/incl [. ='" + file_path + "']") + # if not incTest: + # # 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() + + # Helpful reference? + # def verify_setup(self): + # """ + # Make sure that files/directories are setup with appropriate + # permissions. Aim for defensive coding... make sure all input files + # have permissions of root + # """ + # le_util.make_or_verify_dir(CONFIG.CONFIG_DIR, 0o755) + # le_util.make_or_verify_dir(CONFIG.WORK_DIR, 0o755) + # le_util.make_or_verify_dir(CONFIG.BACKUP_DIR, 0o755) + + def restart(self, quiet=False): + """Restarts nginx server""" + + # May be of use? + # def __add_httpd_transform(self, incl): + # """ + # This function will correctly add a transform to augeas + # The existing augeas.add_transform in python is broken + # """ + # lastInclude = self.aug.match("/augeas/load/Httpd/incl [last()]") + # self.aug.insert(lastInclude[0], "incl", False) + # self.aug.set("/augeas/load/Httpd/incl[last()]", incl) + + def config_test(self): + """Check Configuration""" + return False + + +def main(): + return + +if __name__ == "__main__": + main() diff --git a/letsencrypt/client/plugins/nginx/nginxparser.py b/letsencrypt/client/plugins/nginx/nginxparser.py new file mode 100644 index 000000000..3d01d7ad4 --- /dev/null +++ b/letsencrypt/client/plugins/nginx/nginxparser.py @@ -0,0 +1,110 @@ +"""An nginx config parser based on pyparsing.""" +import string + +from pyparsing import ( + Literal, White, Word, alphanums, CharsNotIn, Forward, Group, + Optional, OneOrMore, ZeroOrMore, pythonStyleComment) + + +class NginxParser(object): + """ + A class that parses nginx configuration with pyparsing + """ + + # constants + left_bracket = Literal("{").suppress() + right_bracket = Literal("}").suppress() + semicolon = Literal(";").suppress() + space = White().suppress() + key = Word(alphanums + "_/") + value = CharsNotIn("{};,") + location = CharsNotIn("{};," + string.whitespace) + # modifier for location uri [ = | ~ | ~* | ^~ ] + modifier = Literal("=") | Literal("~*") | Literal("~") | Literal("^~") + + # rules + assignment = (key + Optional(space + value) + semicolon) + block = Forward() + + block << Group( + Group(key + Optional(space + modifier) + Optional(space + location)) + + left_bracket + + Group(ZeroOrMore(Group(assignment) | block)) + + right_bracket) + + script = OneOrMore(Group(assignment) | block).ignore(pythonStyleComment) + + def __init__(self, source): + self.source = source + + def parse(self): + """ + Returns the parsed tree. + """ + return self.script.parseString(self.source) + + def as_list(self): + """ + Returns the list of tree. + """ + return self.parse().asList() + + +class NginxDumper(object): + """ + A class that dumps nginx configuration from the provided tree. + """ + def __init__(self, blocks, indentation=4): + self.blocks = blocks + self.indentation = indentation + + def __iter__(self, blocks=None, current_indent=0, spacer=' '): + """ + Iterates the dumped nginx content. + """ + blocks = blocks or self.blocks + for key, values in blocks: + if current_indent: + yield spacer + indentation = spacer * current_indent + if isinstance(key, list): + yield indentation + spacer.join(key) + ' {' + for parameter in values: + if isinstance(parameter[0], list): + dumped = self.__iter__( + [parameter], + current_indent + self.indentation) + for line in dumped: + yield line + else: + dumped = spacer.join(parameter) + ';' + yield spacer * ( + current_indent + self.indentation) + dumped + + yield indentation + '}' + else: + yield spacer * current_indent + key + spacer + values + ';' + + def as_string(self): + return '\n'.join(self) + + +# Shortcut functions to respect Python's serialization interface +# (like pyyaml, picker or json) + +def loads(source): + return NginxParser(source).as_list() + + +def load(_file): + return loads(_file.read()) + + +def dumps(blocks, indentation=4): + return NginxDumper(blocks, indentation).as_string() + + +def dump(blocks, _file, indentation=4): + _file.write(dumps(blocks, indentation)) + _file.close() + return _file diff --git a/letsencrypt/client/plugins/nginx/obj.py b/letsencrypt/client/plugins/nginx/obj.py new file mode 100644 index 000000000..69e0d6b20 --- /dev/null +++ b/letsencrypt/client/plugins/nginx/obj.py @@ -0,0 +1,91 @@ +"""Module contains classes used by the Nginx Configurator.""" + + +class Addr(object): + r"""Represents an Nginx VirtualHost address. + + :param str addr: addr part of vhost address + :param str port: port number or \*, or "" + + """ + def __init__(self, tup): + self.tup = tup + + @classmethod + def fromstring(cls, str_addr): + """Initialize Addr from string.""" + tup = str_addr.partition(':') + return cls((tup[0], tup[2])) + + def __str__(self): + if self.tup[1]: + return "%s:%s" % self.tup + return self.tup[0] + + def __eq__(self, other): + if isinstance(other, self.__class__): + return self.tup == other.tup + return False + + def __hash__(self): + return hash(self.tup) + + def get_addr(self): + """Return addr part of Addr object.""" + return self.tup[0] + + def get_port(self): + """Return port.""" + 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)) + + +class VirtualHost(object): # pylint: disable=too-few-public-methods + """Represents an Nginx Virtualhost. + + :ivar str filep: file path of VH + :ivar str path: Augeas path to virtual host + :ivar set addrs: Virtual Host addresses (:class:`set` of :class:`Addr`) + :ivar set names: Server names/aliases of vhost + (:class:`list` of :class:`str`) + + :ivar bool ssl: SSLEngine on in vhost + :ivar bool enabled: Virtual host is enabled + + """ + + def __init__(self, filep, path, addrs, ssl, enabled, names=None): + # pylint: disable=too-many-arguments + """Initialize a VH.""" + self.filep = filep + self.path = path + self.addrs = addrs + self.names = set() if names is None else set(names) + self.ssl = ssl + self.enabled = enabled + + def add_name(self, name): + """Add name to vhost.""" + self.names.add(name) + + def __str__(self): + addr_str = ", ".join(str(addr) for addr in self.addrs) + return ("file: %s\n" + "vh_path: %s\n" + "addrs: %s\n" + "names: %s\n" + "ssl: %s\n" + "enabled: %s" % (self.filep, self.path, addr_str, + self.names, self.ssl, self.enabled)) + + def __eq__(self, other): + if isinstance(other, self.__class__): + return (self.filep == other.filep and self.path == other.path and + self.addrs == other.addrs and + self.names == other.names and + self.ssl == other.ssl and self.enabled == other.enabled) + + return False diff --git a/letsencrypt/client/plugins/nginx/options-ssl.conf b/letsencrypt/client/plugins/nginx/options-ssl.conf new file mode 100644 index 000000000..8380542c0 --- /dev/null +++ b/letsencrypt/client/plugins/nginx/options-ssl.conf @@ -0,0 +1,27 @@ +ssl_session_cache shared:SSL:1m; # 1MB is ~4000 sessions, if it fills old sessions are dropped +ssl_session_timeout 1440m; # Reuse sessions for 24hrs + +# Redirect all traffic to SSL +server { + listen 80 default; + server_name www.example.com example.com; + access_log off; + error_log off; + return 301 https://example.com$request_uri; +} + +server { + listen 443 ssl default_server; + server_name example.com; + + ssl_certificate /path/to/bundle.crt; + ssl_certificate_key /path/to/private.key; + + ssl_protocols TLSv1 TLSv1.1 TLSv1.2; + ssl_prefer_server_ciphers on; + + # Using list of ciphers from "Bulletproof SSL and TLS" + ssl_ciphers "ECDHE-ECDSA-AES128-GCM-SHA256 ECDHE-ECDSA-AES256-GCM-SHA384 ECDHE-ECDSA-AES128-SHA ECDHE-ECDSA-AES256-SHA ECDHE-ECDSA-AES128-SHA256 ECDHE-ECDSA-AES256-SHA384 ECDHE-RSA-AES128-GCM-SHA256 ECDHE-RSA-AES256-GCM-SHA384 ECDHE-RSA-AES128-SHA ECDHE-RSA-AES128-SHA256 ECDHE-RSA-AES256-SHA384 DHE-RSA-AES128-GCM-SHA256 DHE-RSA-AES256-GCM-SHA384 DHE-RSA-AES128-SHA DHE-RSA-AES256-SHA DHE-RSA-AES128-SHA256 DHE-RSA-AES256-SHA256 EDH-RSA-DES-CBC3-SHA"; + + # Normal stuff below here +} diff --git a/letsencrypt/client/plugins/nginx/parser.py b/letsencrypt/client/plugins/nginx/parser.py new file mode 100644 index 000000000..0f95c056c --- /dev/null +++ b/letsencrypt/client/plugins/nginx/parser.py @@ -0,0 +1,413 @@ +"""NginxParser is a member object of the NginxConfigurator class.""" +import os +import re + +from letsencrypt.client import errors + + +class NginxParser(object): + """Class handles the fine details of parsing the Nginx Configuration. + + :ivar str root: Normalized abosulte path to the server root + directory. Without trailing slash. + + """ + + def __init__(self, aug, root, ssl_options): + # Find configuration root and make sure augeas can parse it. + self.aug = aug + self.root = os.path.abspath(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 isinstance(arg, list): + for i, value in enumerate(arg, 1): + self.aug.set( + "%s/directive[last()]/arg[%d]" % (aug_conf_path, i), value) + else: + self.aug.set(aug_conf_path + "/directive[last()]/arg", arg) + + 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 Nginx 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 directive 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 Nginx Include directive into Augeas path. + + Converts an Nginx 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 Nginx 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 nginx 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 + # Nginx 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 Nginx + elif arg.startswith("conf/"): + arg = self.root + arg[4:] + # TODO: Test if Nginx 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 Nginx's basic fnmatch to regular expression. + + :param str clean_fn_match: Nginx style filename match, similar to globs + + :returns: regex suitable for augeas + :rtype: str + + """ + # Checkout fnmatch.py in venv/local/lib/python2.7/fnmatch.py + regex = "" + for letter in clean_fn_match: + if letter == '.': + regex = regex + r"\." + elif letter == '*': + regex = regex + ".*" + # According to nginx.org ? shouldn't appear + # but in case it is valid... + elif letter == '?': + regex = regex + "." + else: + regex = regex + letter + return regex + + def _parse_file(self, filepath): + """Parse file with Augeas + + Checks to see if file_path is parsed by Augeas + If filepath isn't parsed, the file is added and Augeas is reloaded + + :param str filepath: Nginx 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']" % filepath) + if not inc_test: + # Load up files + # This doesn't seem to work on TravisCI + # self.aug.add_transform("Httpd.lns", [filepath]) + self._add_httpd_transform(filepath) + 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 doesn't seem to work for + Travis CI as it loads in libaugeas.so.0.10.0 + + :param str incl: filepath to include for transform + + """ + last_include = self.aug.match("/augeas/load/Httpd/incl [last()]") + if last_include: + # Insert a new node immediately after the last incl + self.aug.insert(last_include[0], "incl", False) + self.aug.set("/augeas/load/Httpd/incl[last()]", incl) + # On first use... must load lens and add file to incl + else: + # Augeas uses base 1 indexing... insert at beginning... + self.aug.set("/augeas/load/Httpd/lens", "Httpd.lns") + self.aug.set("/augeas/load/Httpd/incl", incl) + + 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, excluded in enumerate(excl, 1): + self.aug.set("/augeas/load/Httpd/excl[%d]" % i, excluded) + + self.aug.load() + + 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(root) + + 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 Nginx Configuration Root file.""" + location = ["nginx2.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.LetsEncryptNoInstallationError( + "Could not find configuration root") + + def _set_user_config_file(self, root): + """Set the appropriate user configuration file + + .. todo:: This will have to be updated for other distros versions + + :param str root: pathname which contains the user config + + """ + # Basic check to see if httpd.conf exists and + # in hierarchy via direct include + # httpd.conf was very common as a user file in Nginx 2.2 + if (os.path.isfile(os.path.join(self.root, 'httpd.conf')) and + self.find_dir( + case_i("Include"), case_i("httpd.conf"), root)): + return os.path.join(self.root, 'httpd.conf') + else: + return os.path.join(self.root, 'nginx2.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/plugins/nginx/tests/__init__.py b/letsencrypt/client/plugins/nginx/tests/__init__.py new file mode 100644 index 000000000..157a70759 --- /dev/null +++ b/letsencrypt/client/plugins/nginx/tests/__init__.py @@ -0,0 +1 @@ +"""Let's Encrypt Nginx Tests""" diff --git a/letsencrypt/client/plugins/nginx/tests/configurator_test.py b/letsencrypt/client/plugins/nginx/tests/configurator_test.py new file mode 100644 index 000000000..cb059285a --- /dev/null +++ b/letsencrypt/client/plugins/nginx/tests/configurator_test.py @@ -0,0 +1,196 @@ +"""Test for letsencrypt.client.nginx.configurator.""" +import os +import re +import shutil +import unittest + +import mock + +from letsencrypt.acme import challenges + +from letsencrypt.client import achallenges +from letsencrypt.client import errors +from letsencrypt.client import le_util + +from letsencrypt.client.nginx import configurator +from letsencrypt.client.nginx import obj +from letsencrypt.client.nginx import parser + +from letsencrypt.client.tests.nginx import util + + +class TwoVhost80Test(util.NginxTest): + """Test two standard well configured HTTP vhosts.""" + + def setUp(self): + super(TwoVhost80Test, self).setUp() + + with mock.patch("letsencrypt.client.nginx.configurator." + "mod_loaded") as mock_load: + mock_load.return_value = True + self.config = util.get_nginx_configurator( + self.config_path, self.config_dir, self.work_dir, + self.ssl_options) + + self.vh_truth = util.get_vh_truth( + self.temp_dir, "debian_nginx_2_4/two_vhost_80") + + def tearDown(self): + shutil.rmtree(self.temp_dir) + shutil.rmtree(self.config_dir) + shutil.rmtree(self.work_dir) + + def test_get_all_names(self): + names = self.config.get_all_names() + self.assertEqual(names, set( + ['letsencrypt.demo', 'encryption-example.demo', 'ip-172-30-0-17'])) + + def test_get_virtual_hosts(self): + vhs = self.config.get_virtual_hosts() + self.assertEqual(len(vhs), 4) + found = 0 + + for vhost in vhs: + for truth in self.vh_truth: + if vhost == truth: + found += 1 + break + + self.assertEqual(found, 4) + + def test_is_site_enabled(self): + self.assertTrue(self.config.is_site_enabled(self.vh_truth[0].filep)) + self.assertFalse(self.config.is_site_enabled(self.vh_truth[1].filep)) + self.assertTrue(self.config.is_site_enabled(self.vh_truth[2].filep)) + self.assertTrue(self.config.is_site_enabled(self.vh_truth[3].filep)) + + 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 test_is_name_vhost(self): + 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.assertTrue(self.config.parser.find_dir( + "NameVirtualHost", re.escape("*:443"))) + + def test_make_vhost_ssl(self): + ssl_vhost = self.config.make_vhost_ssl(self.vh_truth[0]) + + self.assertEqual( + ssl_vhost.filep, + os.path.join(self.config_path, "sites-available", + "encryption-example-le-ssl.conf")) + + self.assertEqual(ssl_vhost.path, + "/files" + ssl_vhost.filep + "/IfModule/VirtualHost") + self.assertEqual(len(ssl_vhost.addrs), 1) + self.assertEqual(set([obj.Addr.fromstring("*:443")]), ssl_vhost.addrs) + self.assertEqual(ssl_vhost.names, set(["encryption-example.demo"])) + self.assertTrue(ssl_vhost.ssl) + self.assertFalse(ssl_vhost.enabled) + + self.assertTrue(self.config.parser.find_dir( + "SSLCertificateFile", None, ssl_vhost.path)) + self.assertTrue(self.config.parser.find_dir( + "SSLCertificateKeyFile", None, ssl_vhost.path)) + 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]), + self.config.is_name_vhost(ssl_vhost)) + + self.assertEqual(len(self.config.vhosts), 5) + + @mock.patch("letsencrypt.client.nginx.configurator." + "dvsni.NginxDvsni.perform") + @mock.patch("letsencrypt.client.nginx.configurator." + "NginxConfigurator.restart") + def test_perform(self, mock_restart, mock_dvsni_perform): + # Only tests functionality specific to configurator.perform + # Note: As more challenges are offered this will have to be expanded + auth_key = le_util.Key(self.rsa256_file, self.rsa256_pem) + achall1 = achallenges.DVSNI( + chall=challenges.DVSNI( + r="jIq_Xy1mXGN37tb4L6Xj_es58fW571ZNyXekdZzhh7Q", + nonce="37bc5eb75d3e00a19b4f6355845e5a18"), + domain="encryption-example.demo", key=auth_key) + achall2 = achallenges.DVSNI( + chall=challenges.DVSNI( + r="uqnaPzxtrndteOqtrXb0Asl5gOJfWAnnx6QJyvcmlDU", + nonce="59ed014cac95f77057b1d7a1b2c596ba"), + domain="letsencrypt.demo", key=auth_key) + + dvsni_ret_val = [ + challenges.DVSNIResponse(s="randomS1"), + challenges.DVSNIResponse(s="randomS2"), + ] + + mock_dvsni_perform.return_value = dvsni_ret_val + responses = self.config.perform([achall1, achall2]) + + self.assertEqual(mock_dvsni_perform.call_count, 1) + self.assertEqual(responses, dvsni_ret_val) + + self.assertEqual(mock_restart.call_count, 1) + + @mock.patch("letsencrypt.client.nginx.configurator." + "subprocess.Popen") + def test_get_version(self, mock_popen): + mock_popen().communicate.return_value = ( + "Server Version: Nginx/2.4.2 (Debian)", "") + self.assertEqual(self.config.get_version(), (2, 4, 2)) + + mock_popen().communicate.return_value = ( + "Server Version: Nginx/2 (Linux)", "") + self.assertEqual(self.config.get_version(), (2,)) + + mock_popen().communicate.return_value = ( + "Server Version: Nginx (Debian)", "") + self.assertRaises( + errors.LetsEncryptConfiguratorError, self.config.get_version) + + mock_popen().communicate.return_value = ( + "Server Version: Nginx/2.3\n Nginx/2.4.7", "") + self.assertRaises( + errors.LetsEncryptConfiguratorError, self.config.get_version) + + mock_popen.side_effect = OSError("Can't find program") + self.assertRaises( + errors.LetsEncryptConfiguratorError, self.config.get_version) + + +if __name__ == '__main__': + unittest.main() diff --git a/letsencrypt/client/plugins/nginx/tests/dvsni_test.py b/letsencrypt/client/plugins/nginx/tests/dvsni_test.py new file mode 100644 index 000000000..869b5e806 --- /dev/null +++ b/letsencrypt/client/plugins/nginx/tests/dvsni_test.py @@ -0,0 +1,170 @@ +"""Test for letsencrypt.client.nginx.dvsni.""" +import pkg_resources +import unittest +import shutil + +import mock + +from letsencrypt.acme import challenges + +from letsencrypt.client import achallenges +from letsencrypt.client import le_util + +from letsencrypt.client.nginx.obj import Addr + +from letsencrypt.client.tests.nginx import util + + +class DvsniPerformTest(util.NginxTest): + """Test the NginxDVSNI challenge.""" + + def setUp(self): + super(DvsniPerformTest, self).setUp() + + with mock.patch("letsencrypt.client.nginx.configurator." + "mod_loaded") as mock_load: + mock_load.return_value = True + config = util.get_nginx_configurator( + self.config_path, self.config_dir, self.work_dir, + self.ssl_options) + + from letsencrypt.client.nginx import dvsni + self.sni = dvsni.NginxDvsni(config) + + rsa256_file = pkg_resources.resource_filename( + "letsencrypt.client.tests", 'testdata/rsa256_key.pem') + rsa256_pem = pkg_resources.resource_string( + "letsencrypt.client.tests", 'testdata/rsa256_key.pem') + + auth_key = le_util.Key(rsa256_file, rsa256_pem) + self.achalls = [ + achallenges.DVSNI( + chall=challenges.DVSNI( + r="\x8c\x8a\xbf_-f\\cw\xee\xd6\xf8/\xa5\xe3\xfd\xeb9\xf1" + "\xf5\xb9\xefVM\xc9w\xa4u\x9c\xe1\x87\xb4", + nonce="7\xbc^\xb7]>\x00\xa1\x9bOcU\x84^Z\x18", + ), domain="encryption-example.demo", key=auth_key), + achallenges.DVSNI( + chall=challenges.DVSNI( + r="\xba\xa9\xda?=1.5.5', 'pyrfc3339', 'python-augeas', 'python2-pythondialog>=3.2.2rc1', # Debian squeeze support, cf. #280 @@ -103,6 +105,8 @@ setup( 'letsencrypt.client.plugins', 'letsencrypt.client.plugins.apache', 'letsencrypt.client.plugins.apache.tests', + 'letsencrypt.client.plugins.nginx', + 'letsencrypt.client.plugins.nginx.tests', 'letsencrypt.client.plugins.standalone', 'letsencrypt.client.plugins.standalone.tests', 'letsencrypt.client.tests', From 37649966c20f6aeab2c44ad74e24a315a80347d4 Mon Sep 17 00:00:00 2001 From: yan Date: Thu, 2 Apr 2015 18:15:17 -0700 Subject: [PATCH 02/38] Nginx versioning and other config changes --- letsencrypt/client/constants.py | 6 + letsencrypt/client/interfaces.py | 8 + .../client/plugins/nginx/configurator.py | 235 +++--------------- letsencrypt/scripts/main.py | 7 + setup.py | 2 + 5 files changed, 58 insertions(+), 200 deletions(-) diff --git a/letsencrypt/client/constants.py b/letsencrypt/client/constants.py index 43cf5e8a0..02fab62cb 100644 --- a/letsencrypt/client/constants.py +++ b/letsencrypt/client/constants.py @@ -40,6 +40,12 @@ APACHE_REWRITE_HTTPS_ARGS = [ """Apache rewrite rule arguments used for redirections to https vhost""" +NGINX_MOD_SSL_CONF = pkg_resources.resource_filename( + "letsencrypt.client.plugins.nginx", "options-ssl.conf") +"""Path to the Nginx mod_ssl config file found in the Let's Encrypt +distribution.""" + + DVSNI_CHALLENGE_PORT = 443 """Port to perform DVSNI challenge.""" diff --git a/letsencrypt/client/interfaces.py b/letsencrypt/client/interfaces.py index 0f032a92e..3d3001377 100644 --- a/letsencrypt/client/interfaces.py +++ b/letsencrypt/client/interfaces.py @@ -129,6 +129,14 @@ class IConfig(zope.interface.Interface): apache_mod_ssl_conf = zope.interface.Attribute( "Contains standard Apache SSL directives.") + nginx_server_root = zope.interface.Attribute( + "Nginx server root directory.") + nginx_ctl = zope.interface.Attribute( + "Path to the 'nginx' binary, used for 'configtest' and " + "retrieving nginx version number.") + nginx_mod_ssl_conf = zope.interface.Attribute( + "Contains standard nginx SSL directives.") + class IInstaller(zope.interface.Interface): """Generic Let's Encrypt Installer Interface. diff --git a/letsencrypt/client/plugins/nginx/configurator.py b/letsencrypt/client/plugins/nginx/configurator.py index 240dbe55e..bb1bb8a34 100644 --- a/letsencrypt/client/plugins/nginx/configurator.py +++ b/letsencrypt/client/plugins/nginx/configurator.py @@ -1,4 +1,4 @@ -"""Nginx Configuration based off of Augeas Configurator.""" +"""Nginx Configuration""" import logging import os import re @@ -12,7 +12,6 @@ import zope.interface from letsencrypt.acme import challenges from letsencrypt.client import achallenges -from letsencrypt.client import augeas_configurator from letsencrypt.client import constants from letsencrypt.client import errors from letsencrypt.client import interfaces @@ -23,46 +22,11 @@ from letsencrypt.client.plugins.nginx import obj from letsencrypt.client.plugins.nginx import parser -# TODO: Augeas sections ie. , beginning and closing -# tags need to be the same case, otherwise Augeas doesn't recognize them. -# This is not able to be completely remedied by regular expressions because -# 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 LE's transactions will ensure -# only properly formed sections are added. - -# Note: This protocol works for filenames with spaces in it, the sites are -# properly set up and directives are changed appropriately, but Nginx won't -# recognize names in sites-enabled that have spaces. These are not added to the -# Nginx configuration. It may be wise to warn the user if they are trying -# to use vhost filenames that contain spaces and offer to change ' ' to '_' - -# Note: FILEPATHS and changes to files are transactional. They are copied -# over before the updates are made to the existing files. NEW_FILES is -# transactional due to the use of register_file_creation() - - -class NginxConfigurator(augeas_configurator.AugeasConfigurator): +class NginxConfigurator(object): # pylint: disable=too-many-instance-attributes,too-many-public-methods """Nginx configurator. - State of Configurator: This code has been tested under Ubuntu 12.04 - Nginx 2.2 and this code works for Ubuntu 14.04 Nginx 2.4. Further - notes below. - - This class was originally developed for Nginx 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. - - .. 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 clients are clarified with the new and developing protocol. + .. todo:: Add proper support for comments in the config :ivar config: Configuration. :type config: :class:`~letsencrypt.client.interfaces.IConfig` @@ -89,7 +53,7 @@ class NginxConfigurator(augeas_configurator.AugeasConfigurator): (used mostly for unittesting) """ - super(NginxConfigurator, self).__init__(config) + self.config = config # Verify that all directories and files exist with proper permissions if os.geteuid() == 0: @@ -109,10 +73,8 @@ class NginxConfigurator(augeas_configurator.AugeasConfigurator): def prepare(self): """Prepare the authenticator/installer.""" self.parser = parser.NginxParser( - self.aug, self.config.nginx_server_root, + self.config.nginx_server_root, self.config.nginx_mod_ssl_conf) - # Check for errors in parsing files with Augeas - self.check_parsing_errors("httpd.aug") # Set Version if self.version is None: @@ -121,13 +83,6 @@ class NginxConfigurator(augeas_configurator.AugeasConfigurator): # Get all of the available vhosts self.vhosts = self.get_virtual_hosts() - # Enable mod_ssl if it isn't already enabled - # This is Let's Encrypt... we enable mod_ssl on initialization :) - # TODO: attempt to make the check faster... this enable should - # be asynchronous as it shouldn't be that time sensitive - # on initialization - self._prepare_server_https() - temp_install(self.config.nginx_mod_ssl_conf) def deploy_cert(self, domain, cert, key, cert_chain=None): @@ -278,50 +233,6 @@ class NginxConfigurator(augeas_configurator.AugeasConfigurator): return all_names - 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.plugins.nginx.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 _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.client.plugins.nginx.obj.VirtualHost` - - """ - addrs = set() - args = self.aug.match(path + "/arg") - for arg in args: - addrs.add(obj.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 - # TODO: make "sites-available" a configurable directory def get_virtual_hosts(self): """Returns list of virtual hosts found in the Nginx configuration. @@ -332,40 +243,15 @@ class NginxConfigurator(augeas_configurator.AugeasConfigurator): :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")))) + # Search sites-available/, conf.d/, nginx.conf for possible vhosts + paths = self.parser.get_conf_files() vhs = [] for path in paths: - vhs.append(self._create_vhost(path)) + vhs.append(self.parser.get_vhosts(path)) return vhs - def is_name_vhost(self, target_addr): - r"""Returns if vhost is a name based vhost - - NameVirtualHost was deprecated in Nginx 2.4 as all VirtualHosts are - now NameVirtualHosts. If version is earlier than 2.4, check if addr - has a NameVirtualHost directive in the Nginx 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 Nginx 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 add_name_vhost(self, addr): """Adds NameVirtualHost directive for given address. @@ -379,55 +265,6 @@ class NginxConfigurator(augeas_configurator.AugeasConfigurator): self.save_notes += "Setting %s to be NameBasedVirtualHost\n" % addr self.save_notes += "\tDirective added to %s\n" % path - 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 mod_loaded("ssl_module", self.config.nginx_ctl): - logging.info("Loading mod_ssl into Nginx Server") - enable_mod("ssl", self.config.nginx_init_script, - self.config.nginx_enmod) - - # 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: - logging.debug("No Listen 443 directive found") - logging.debug("Setting the Nginx 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_server_sni_ready(self, vhost, default_addr="*:443"): - """Checks to see if the server is ready for SNI challenges. - - :param vhost: VirtualHost to check SNI compatibility - :type vhost: :class:`~letsencrypt.client.plugins.nginx.obj.VirtualHost` - - :param str default_addr: TODO - investigate function further - - """ - if self.version >= (2, 4): - return - # Check for NameVirtualHost - # First see if any of the vhost addresses is a _default_ addr - for addr in vhost.addrs: - if addr.get_addr() == "_default_": - if not self.is_name_vhost(default_addr): - logging.debug("Setting all VirtualHosts on %s to be " - "name based vhosts", default_addr) - self.add_name_vhost(default_addr) - - # No default addresses... so set each one individually - for addr in vhost.addrs: - if not self.is_name_vhost(addr): - logging.debug("Setting VirtualHost at %s to be a name " - "based virtual host", addr) - self.add_name_vhost(addr) - def make_vhost_ssl(self, nonssl_vhost): # pylint: disable=too-many-locals """Makes an ssl_vhost version of a nonssl_vhost. @@ -504,23 +341,6 @@ class NginxConfigurator(augeas_configurator.AugeasConfigurator): 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) - logging.info("Enabling NameVirtualHosts on %s", addr) - need_to_save = True - - if need_to_save: - self.save() - return ssl_vhost def supported_enhancements(self): # pylint: disable=no-self-use @@ -908,17 +728,17 @@ class NginxConfigurator(augeas_configurator.AugeasConfigurator): """ try: proc = subprocess.Popen( - ["sudo", self.config.nginx_ctl, "configtest"], # TODO: sudo? + [self.config.nginx_ctl, "-t"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) stdout, stderr = proc.communicate() except (OSError, ValueError): - logging.fatal("Unable to run /usr/sbin/nginx2ctl configtest") + logging.fatal("Unable to run nginx config test") sys.exit(1) if proc.returncode != 0: # Enter recovery routine... - logging.error("Configtest failed") + logging.error("Config test failed") logging.error(stdout) logging.error(stderr) return False @@ -947,27 +767,42 @@ class NginxConfigurator(augeas_configurator.AugeasConfigurator): :rtype: tuple :raises errors.LetsEncryptConfiguratorError: - Unable to find Nginx version + Unable to find Nginx version or version is unsupported """ try: proc = subprocess.Popen( - [self.config.nginx_ctl, "-v"], + [self.config.nginx_ctl, "-V"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) - text = proc.communicate()[0] + text = proc.communicate()[1] # nginx prints output to stderr except (OSError, ValueError): raise errors.LetsEncryptConfiguratorError( - "Unable to run %s -v" % self.config.nginx_ctl) + "Unable to run %s -V" % self.config.nginx_ctl) - regex = re.compile(r"Nginx/([0-9\.]*)", re.IGNORECASE) - matches = regex.findall(text) + version_regex = re.compile(r"nginx/([0-9\.]*)", re.IGNORECASE) + version_matches = version_regex.findall(text) - if len(matches) != 1: + sni_regex = re.compile(r"TLS SNI support enabled", re.IGNORECASE) + sni_matches = sni_regex.findall(text) + + if len(version_matches) == 0: raise errors.LetsEncryptConfiguratorError( "Unable to find Nginx version") + if len(sni_matches) == 0: + raise errors.LetsEncryptConfiguratorError( + "Nginx build doesn't support SNI") - return tuple([int(i) for i in matches[0].split(".")]) + nginx_version = tuple([int(i) for i in version_matches[0].split(".")]) + + # nginx <= 0.7.14 has an incompatible SSL configuration format + if (nginx_version[0] == 0 and + (nginx_version[1] < 7 or + (nginx_version[1] == 7 and nginx_version[2] < 15))): + raise errors.LetsEncryptConfiguratorError( + "Nginx version not supported") + + return nginx_version def more_info(self): """Human-readable string to help understand the module""" @@ -1160,4 +995,4 @@ def temp_install(options_ssl): # Check to make sure options-ssl.conf is installed if not os.path.isfile(options_ssl): - shutil.copyfile(constants.APACHE_MOD_SSL_CONF, options_ssl) + shutil.copyfile(constants.NGINX_MOD_SSL_CONF, options_ssl) diff --git a/letsencrypt/scripts/main.py b/letsencrypt/scripts/main.py index 20813f11e..9da8c30b0 100644 --- a/letsencrypt/scripts/main.py +++ b/letsencrypt/scripts/main.py @@ -125,6 +125,13 @@ def create_parser(): add("--apache-init-script", default="/etc/init.d/apache2", help=config_help("apache_init_script")) + add("--nginx-server-root", default="/etc/nginx", + help=config_help("nginx_server_root")) + add("--nginx-mod-ssl-conf", + default="/etc/letsencrypt/options-ssl-nginx.conf", + help=config_help("nginx_mod_ssl_conf")) + add("--nginx-ctl", default="nginx", help=config_help("nginx_ctl")) + return parser diff --git a/setup.py b/setup.py index 4aeed5508..258992bae 100644 --- a/setup.py +++ b/setup.py @@ -132,6 +132,8 @@ setup( 'letsencrypt.authenticators': [ 'apache = letsencrypt.client.plugins.apache.configurator' ':ApacheConfigurator', + 'nginx = letsencrypt.client.plugins.nginx.configurator' + ':NginxConfigurator', 'standalone = letsencrypt.client.plugins.standalone.authenticator' ':StandaloneAuthenticator', ], From 33ff366171f10bf11249fd32e60f4e1545b1413b Mon Sep 17 00:00:00 2001 From: yan Date: Fri, 3 Apr 2015 14:56:04 -0700 Subject: [PATCH 03/38] Remove redirect enhancement, fix reload --- .../client/plugins/nginx/configurator.py | 344 +----------------- 1 file changed, 6 insertions(+), 338 deletions(-) diff --git a/letsencrypt/client/plugins/nginx/configurator.py b/letsencrypt/client/plugins/nginx/configurator.py index bb1bb8a34..51275a6ee 100644 --- a/letsencrypt/client/plugins/nginx/configurator.py +++ b/letsencrypt/client/plugins/nginx/configurator.py @@ -68,7 +68,7 @@ class NginxConfigurator(object): self.parser = None self.version = version self.vhosts = None - self._enhance_func = {"redirect": self._enable_redirect} + self._enhance_func = {} # TODO: Support at least redirects def prepare(self): """Prepare the authenticator/installer.""" @@ -345,7 +345,7 @@ class NginxConfigurator(object): def supported_enhancements(self): # pylint: disable=no-self-use """Returns currently supported enhancements.""" - return ["redirect"] + return [] def enhance(self, domain, enhancement, options=None): """Enhance configuration. @@ -367,270 +367,6 @@ class NginxConfigurator(object): except errors.LetsEncryptConfiguratorError: logging.warn("Failed %s for %s", enhancement, domain) - def _enable_redirect(self, ssl_vhost, unused_options): - """Redirect all equivalent HTTP traffic to ssl_vhost. - - .. todo:: This enhancement should be rewritten and will - unfortunately require lots of debugging by hand. - - Adds Redirect directive to the port 80 equivalent of ssl_vhost - First the function attempts to find the vhost with equivalent - ip addresses that serves on non-ssl ports - The function then adds the directive - - .. note:: This function saves the configuration - - :param ssl_vhost: Destination of traffic, an ssl enabled vhost - :type ssl_vhost: - :class:`~letsencrypt.client.plugins.nginx.obj.VirtualHost` - - :param unused_options: Not currently used - :type unused_options: Not Available - - :returns: Success, general_vhost (HTTP vhost) - :rtype: (bool, - :class:`~letsencrypt.client.plugins.nginx.obj.VirtualHost`) - - """ - if not mod_loaded("rewrite_module", self.config.nginx_ctl): - enable_mod("rewrite", self.config.nginx_init_script, - self.config.nginx_enmod) - - general_v = self._general_vhost(ssl_vhost) - if general_v is None: - # Add virtual_server with redirect - logging.debug( - "Did not find http version of ssl virtual host... creating") - return self._create_redirect_vhost(ssl_vhost) - else: - # Check if redirection already exists - exists, code = self._existing_redirect(general_v) - if exists: - if code == 0: - logging.debug("Redirect already added") - logging.info( - "Configuration is already redirecting traffic to HTTPS") - return - else: - logging.info("Unknown redirect exists for this vhost") - raise errors.LetsEncryptConfiguratorError( - "Unknown redirect already exists " - "in {}".format(general_v.filep)) - # Add directives to server - self.parser.add_dir(general_v.path, "RewriteEngine", "On") - self.parser.add_dir(general_v.path, "RewriteRule", - constants.APACHE_REWRITE_HTTPS_ARGS) - self.save_notes += ("Redirecting host in %s to ssl vhost in %s\n" % - (general_v.filep, ssl_vhost.filep)) - self.save() - - logging.info("Redirecting vhost in %s to ssl vhost in %s", - general_v.filep, ssl_vhost.filep) - - 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.client.plugins.nginx.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.APACHE_REWRITE_HTTPS_ARGS): - for idx, match in enumerate(rewrite_path): - if (self.aug.get(match) != - constants.APACHE_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 _create_redirect_vhost(self, ssl_vhost): - """Creates an http_vhost specifically to redirect for the ssl_vhost. - - :param ssl_vhost: ssl vhost - :type ssl_vhost: - :class:`~letsencrypt.client.plugins.nginx.obj.VirtualHost` - - :returns: tuple of the form - (`success`, - :class:`~letsencrypt.client.plugins.nginx.obj.VirtualHost`) - :rtype: tuple - - """ - # Consider changing this to a dictionary check - # Make sure adding the vhost will be safe - conflict, host_or_addrs = self._conflicting_host(ssl_vhost) - if conflict: - raise errors.LetsEncryptConfiguratorError( - "Unable to create a redirection vhost " - "- {}".format(host_or_addrs)) - - redirect_addrs = host_or_addrs - - # get servernames and serveraliases - serveralias = "" - servername = "" - size_n = len(ssl_vhost.names) - if size_n > 0: - servername = "ServerName " + ssl_vhost.names[0] - if size_n > 1: - serveralias = " ".join(ssl_vhost.names[1:size_n]) - serveralias = "ServerAlias " + serveralias - redirect_file = ("\n" - "%s \n" - "%s \n" - "ServerSignature Off\n" - "\n" - "RewriteEngine On\n" - "RewriteRule %s\n" - "\n" - "ErrorLog /var/log/nginx2/redirect.error.log\n" - "LogLevel warn\n" - "\n" - % (servername, serveralias, - " ".join(constants.APACHE_REWRITE_HTTPS_ARGS))) - - # Write out the file - # This is the default name - redirect_filename = "le-redirect.conf" - - # See if a more appropriate name can be applied - if len(ssl_vhost.names) > 0: - # Sanity check... - # make sure servername doesn't exceed filename length restriction - if ssl_vhost.names[0] < (255-23): - redirect_filename = "le-redirect-%s.conf" % ssl_vhost.names[0] - - redirect_filepath = os.path.join( - self.parser.root, "sites-available", redirect_filename) - - # Register the new file that will be created - # Note: always register the creation before writing to ensure file will - # be removed in case of unexpected program exit - self.reverter.register_file_creation(False, redirect_filepath) - - # Write out file - with open(redirect_filepath, "w") as redirect_fd: - redirect_fd.write(redirect_file) - logging.info("Created redirect file: %s", redirect_filename) - - self.aug.load() - # Make a new vhost data structure and add it to the lists - new_vhost = self._create_vhost(parser.get_aug_path(redirect_filepath)) - self.vhosts.append(new_vhost) - - # Finally create documentation for the change - self.save_notes += ("Created a port 80 vhost, %s, for redirection to " - "ssl vhost %s\n" % - (new_vhost.filep, ssl_vhost.filep)) - - def _conflicting_host(self, ssl_vhost): - """Checks for conflicting HTTP vhost for ssl_vhost. - - Checks for a conflicting host, such that a new port 80 host could not - be created without ruining the nginx config - Used with redirection - - returns: conflict, host_or_addrs - boolean - if conflict: returns conflicting vhost - 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.plugins.nginx.obj.VirtualHost` - - :returns: TODO - :rtype: TODO - - """ - # Consider changing this to a dictionary check - redirect_addrs = "" - for ssl_a in ssl_vhost.addrs: - # Add space on each new addr, combine "VirtualHost"+redirect_addrs - redirect_addrs = redirect_addrs + " " - ssl_a_vhttp = ssl_a.get_addr_obj("80") - # Search for a conflicting host... - for vhost in self.vhosts: - if vhost.enabled: - if (ssl_a_vhttp in vhost.addrs or - ssl_a.get_addr_obj("") in vhost.addrs or - ssl_a.get_addr_obj("*") in vhost.addrs): - # We have found a conflicting host... just return - return True, vhost - - redirect_addrs = redirect_addrs + ssl_a_vhttp - - return False, redirect_addrs - - def _general_vhost(self, ssl_vhost): - """Find appropriate HTTP vhost for ssl_vhost. - - Function needs to be thoroughly tested and perhaps improved - Will not do well with malformed configurations - Consider changing this into a dict check - - :param ssl_vhost: ssl vhost to check - :type ssl_vhost: - :class:`~letsencrypt.client.plugins.nginx.obj.VirtualHost` - - :returns: HTTP vhost or None if unsuccessful - :rtype: :class:`~letsencrypt.client.plugins.nginx.obj.VirtualHost` - or None - - """ - # _default_:443 check - # Instead... should look for vhost of the form *:80 - # Should we prompt the user? - ssl_addrs = ssl_vhost.addrs - if ssl_addrs == obj.Addr.fromstring("_default_:443"): - ssl_addrs = [obj.Addr.fromstring("*:443")] - - for vhost in self.vhosts: - found = 0 - # Not the same vhost, and same number of addresses - if vhost != ssl_vhost and len(vhost.addrs) == len(ssl_vhost.addrs): - # Find each address in ssl_host in test_host - for ssl_a in ssl_addrs: - for test_a in vhost.addrs: - if test_a.get_addr() == ssl_a.get_addr(): - # Check if found... - if (test_a.get_port() == "80" or - test_a.get_port() == "" or - test_a.get_port() == "*"): - found += 1 - break - # Check to make sure all addresses were found - # and names are equal - if (found == len(ssl_vhost.addrs) and - vhost.names == ssl_vhost.names): - return vhost - return None - def get_all_certs_keys(self): """Find all existing keys, certs from configuration. @@ -717,7 +453,7 @@ class NginxConfigurator(object): :rtype: bool """ - return nginx_restart(self.config.nginx_init_script) + return nginx_restart(self.config.nginx_ctl) def config_test(self): # pylint: disable=no-self-use """Check the configuration of Nginx for errors. @@ -863,82 +599,14 @@ class NginxConfigurator(object): self.restart() -def enable_mod(mod_name, nginx_init_script, nginx_enmod): - """Enables module in Nginx. - - Both enables and restarts Nginx so module is active. - - :param str mod_name: Name of the module to enable. - :param str nginx_init_script: Path to the Nginx init script. - :param str nginx_enmod: Path to the Nginx a2enmod script. - - """ - try: - # Use check_output so the command will finish before reloading - # TODO: a2enmod is debian specific... - subprocess.check_call(["sudo", nginx_enmod, mod_name], # TODO: sudo? - stdout=open("/dev/null", "w"), - stderr=open("/dev/null", "w")) - nginx_restart(nginx_init_script) - except (OSError, subprocess.CalledProcessError) as err: - logging.error("Error enabling mod_%s", mod_name) - logging.error("Exception: %s", err) - sys.exit(1) - - -def mod_loaded(module, nginx_ctl): - """Checks to see if mod_ssl is loaded - - Uses ``nginx_ctl`` to get loaded module list. This also effectively - serves as a config_test. - - :param str nginx_ctl: Path to nginx2ctl binary. - - :returns: If ssl_module is included and active in Nginx - :rtype: bool - - """ - try: - proc = subprocess.Popen( - [nginx_ctl, "-M"], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE) - stdout, stderr = proc.communicate() - - except (OSError, ValueError): - logging.error( - "Error accessing %s for loaded modules!", nginx_ctl) - raise errors.LetsEncryptConfiguratorError( - "Error accessing loaded modules") - # Small errors that do not impede - if proc.returncode != 0: - logging.warn("Error in checking loaded module list: %s", stderr) - raise errors.LetsEncryptMisconfigurationError( - "Nginx is unable to check whether or not the module is " - "loaded because Nginx is misconfigured.") - - if module in stdout: - return True - return False - - -def nginx_restart(nginx_init_script): +def nginx_restart(nginx_ctl): """Restarts the Nginx Server. - :param str nginx_init_script: Path to the Nginx init script. - - .. todo:: Try to use reload instead. (This caused timing problems before) - - .. todo:: On failure, this should be a recovery_routine call with another - restart. This will confuse and inhibit developers from testing code - though. This change should happen after - the NginxConfigurator has been thoroughly tested. The function will - need to be moved into the class again. Perhaps - this version can live on... for testing purposes. + :param str nginx_ctl: Path to the Nginx binary. """ try: - proc = subprocess.Popen([nginx_init_script, "restart"], + proc = subprocess.Popen([nginx_ctl, "-s", "reload"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) stdout, stderr = proc.communicate() From 2460f85dbec0b2409ace57f54af6ba8ecdca3a3b Mon Sep 17 00:00:00 2001 From: yan Date: Fri, 3 Apr 2015 16:01:44 -0700 Subject: [PATCH 04/38] Add save and reverter methods --- .../client/plugins/nginx/configurator.py | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/letsencrypt/client/plugins/nginx/configurator.py b/letsencrypt/client/plugins/nginx/configurator.py index 51275a6ee..ae93b24b5 100644 --- a/letsencrypt/client/plugins/nginx/configurator.py +++ b/letsencrypt/client/plugins/nginx/configurator.py @@ -16,6 +16,7 @@ from letsencrypt.client import constants from letsencrypt.client import errors from letsencrypt.client import interfaces from letsencrypt.client import le_util +from letsencrypt.client import reverter from letsencrypt.client.plugins.nginx import dvsni from letsencrypt.client.plugins.nginx import obj @@ -54,6 +55,7 @@ class NginxConfigurator(object): """ self.config = config + self.save_notes = "" # Verify that all directories and files exist with proper permissions if os.geteuid() == 0: @@ -70,6 +72,10 @@ class NginxConfigurator(object): self.vhosts = None self._enhance_func = {} # TODO: Support at least redirects + # Set up reverter + self.reverter = reverter.Reverter(config) + self.reverter.recovery_routine() + def prepare(self): """Prepare the authenticator/installer.""" self.parser = parser.NginxParser( @@ -550,6 +556,66 @@ class NginxConfigurator(object): version=".".join(str(i) for i in self.version)) ) + # Wrapper functions for Reverter class + def save(self, title=None, temporary=False): + """Saves all changes to the configuration files. + + Working changes are saved in *.conf.le files. This overrides the .conf + file with the .conf.le file contents. + + :param str title: The title of the save. If a title is given, the + configuration will be saved as a new checkpoint and put in a + timestamped directory. + + :param bool temporary: Indicates whether the changes made will + be quickly reversed in the future (ie. challenges) + + """ + if len(self.save_files) > 0: + # Create Checkpoint + if temporary: + self.reverter.add_to_temp_checkpoint( + self.save_files, self.save_notes) + else: + self.reverter.add_to_checkpoint(self.save_files, + self.save_notes) + # Override the original files with their working copies + for f in self.save_files: + tmpfile = f + '.le' + if (os.path.isfile(tmpfile)): + os.rename(f + '.le', f) + else: + logging.warn("Expected file %s to exist", tmpfile) + + if title and not temporary: + self.reverter.finalize_checkpoint(title) + + return True + + def recovery_routine(self): + """Revert all previously modified files. + + Reverts all modified files that have not been saved as a checkpoint + + """ + self.reverter.recovery_routine() + + def revert_challenge_config(self): + """Used to cleanup challenge configurations.""" + self.reverter.revert_temporary_config() + + def rollback_checkpoints(self, rollback=1): + """Rollback saved checkpoints. + + :param int rollback: Number of checkpoints to revert + + """ + self.reverter.rollback_checkpoints(rollback) + + def view_config_changes(self): + """Show all of the configuration changes that have taken place.""" + self.reverter.view_config_changes() + ########################################################################### # Challenges Section ########################################################################### From d36d0eeb30b342550b1210a133c830ab96bcd18d Mon Sep 17 00:00:00 2001 From: yan Date: Fri, 3 Apr 2015 16:21:45 -0700 Subject: [PATCH 05/38] Group nginx configurator methods more logically --- .../client/plugins/nginx/configurator.py | 105 ++++++------------ 1 file changed, 31 insertions(+), 74 deletions(-) diff --git a/letsencrypt/client/plugins/nginx/configurator.py b/letsencrypt/client/plugins/nginx/configurator.py index ae93b24b5..624d24ca9 100644 --- a/letsencrypt/client/plugins/nginx/configurator.py +++ b/letsencrypt/client/plugins/nginx/configurator.py @@ -156,6 +156,9 @@ class NginxConfigurator(object): if not vhost.enabled: self.enable_site(vhost) + ####################### + # Vhost parsing methods + ####################### def choose_vhost(self, target_name): """Chooses a virtual host based on the given domain name. @@ -258,19 +261,6 @@ class NginxConfigurator(object): return vhs - def add_name_vhost(self, addr): - """Adds NameVirtualHost directive for given address. - - :param str addr: Address that will be added as NameVirtualHost directive - - """ - 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 make_vhost_ssl(self, nonssl_vhost): # pylint: disable=too-many-locals """Makes an ssl_vhost version of a nonssl_vhost. @@ -349,6 +339,29 @@ class NginxConfigurator(object): return ssl_vhost + def get_all_certs_keys(self): + """Find all existing keys, certs from configuration. + + Retrieve all certs and keys set in VirtualHosts on the Nginx 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: + # TODO: get the cert, key, and conf file paths + + return c_k + + ##################### + # enhancement methods + ##################### def supported_enhancements(self): # pylint: disable=no-self-use """Returns currently supported enhancements.""" return [] @@ -373,39 +386,9 @@ class NginxConfigurator(object): except errors.LetsEncryptConfiguratorError: logging.warn("Failed %s for %s", enhancement, domain) - def get_all_certs_keys(self): - """Find all existing keys, certs from configuration. - - Retrieve all certs and keys set in VirtualHosts on the Nginx 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: - logging.error("Too many cert or key directives in vhost %s", - vhost.filep) - sys.exit(40) - - 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 - + ######################### + # Nginx server management + ######################### def is_site_enabled(self, avail_fp): """Checks to see if the given site is enabled. @@ -556,7 +539,9 @@ class NginxConfigurator(object): version=".".join(str(i) for i in self.version)) ) + ###################################### # Wrapper functions for Reverter class + ###################################### def save(self, title=None, temporary=False): """Saves all changes to the configuration files. @@ -692,34 +677,6 @@ def nginx_restart(nginx_ctl): return True -def get_file_path(vhost_path): - """Get file path from augeas_vhost_path. - - Takes in Augeas path and returns the file name - - :param str vhost_path: Augeas virtual host path - - :returns: filename of vhost - :rtype: str - - """ - # Strip off /files - avail_fp = vhost_path[6:] - # This can be optimized... - while True: - # Cast both to lowercase to be case insensitive - find_if = avail_fp.lower().find("/ifmodule") - if find_if != -1: - avail_fp = avail_fp[:find_if] - continue - find_vh = avail_fp.lower().find("/virtualhost") - if find_vh != -1: - avail_fp = avail_fp[:find_vh] - continue - break - return avail_fp - - def temp_install(options_ssl): """Temporary install for convenience.""" # WARNING: THIS IS A POTENTIAL SECURITY VULNERABILITY From 8caf03dcbb519025a60d1b76cfcbc7f119b125c6 Mon Sep 17 00:00:00 2001 From: yan Date: Mon, 6 Apr 2015 11:24:03 -0700 Subject: [PATCH 06/38] Update nginxparser test, remove other tests for now --- .../plugins/nginx/tests/configurator_test.py | 38 ++-- .../client/plugins/nginx/tests/dvsni_test.py | 170 ------------------ .../plugins/nginx/tests/nginxparser_test.py | 12 +- .../client/plugins/nginx/tests/obj_test.py | 68 ------- .../client/plugins/nginx/tests/parser_test.py | 129 ------------- .../nginx/tests/testdata/nginx.new.conf | 2 +- .../client/plugins/nginx/tests/util.py | 17 +- 7 files changed, 45 insertions(+), 391 deletions(-) delete mode 100644 letsencrypt/client/plugins/nginx/tests/dvsni_test.py delete mode 100644 letsencrypt/client/plugins/nginx/tests/obj_test.py delete mode 100644 letsencrypt/client/plugins/nginx/tests/parser_test.py diff --git a/letsencrypt/client/plugins/nginx/tests/configurator_test.py b/letsencrypt/client/plugins/nginx/tests/configurator_test.py index cb059285a..6b2612616 100644 --- a/letsencrypt/client/plugins/nginx/tests/configurator_test.py +++ b/letsencrypt/client/plugins/nginx/tests/configurator_test.py @@ -1,4 +1,4 @@ -"""Test for letsencrypt.client.nginx.configurator.""" +"""Test for letsencrypt.client.plugins.nginx.configurator.""" import os import re import shutil @@ -12,11 +12,11 @@ from letsencrypt.client import achallenges from letsencrypt.client import errors from letsencrypt.client import le_util -from letsencrypt.client.nginx import configurator -from letsencrypt.client.nginx import obj -from letsencrypt.client.nginx import parser +from letsencrypt.client.plugins.nginx import configurator +from letsencrypt.client.plugins.nginx import obj +from letsencrypt.client.plugins.nginx import parser -from letsencrypt.client.tests.nginx import util +from letsencrypt.client.plugins.nginx.tests import util class TwoVhost80Test(util.NginxTest): @@ -25,7 +25,7 @@ class TwoVhost80Test(util.NginxTest): def setUp(self): super(TwoVhost80Test, self).setUp() - with mock.patch("letsencrypt.client.nginx.configurator." + with mock.patch("letsencrypt.client.plugins.nginx.configurator." "mod_loaded") as mock_load: mock_load.return_value = True self.config = util.get_nginx_configurator( @@ -43,9 +43,15 @@ class TwoVhost80Test(util.NginxTest): def test_get_all_names(self): names = self.config.get_all_names() 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_get_virtual_hosts(self): + """Make sure all vhosts are being properly found. + + .. note:: If test fails, only finding 1 Vhost... it is likely that + it is a problem with is_enabled. + + """ vhs = self.config.get_virtual_hosts() self.assertEqual(len(vhs), 4) found = 0 @@ -59,6 +65,14 @@ class TwoVhost80Test(util.NginxTest): self.assertEqual(found, 4) def test_is_site_enabled(self): + """Test if site is enabled. + + .. note:: This test currently fails for hard links + (which may happen if you move dirs incorrectly) + .. warning:: This test does not work when running using the + unittest.main() function. It incorrectly copies symlinks. + + """ self.assertTrue(self.config.is_site_enabled(self.vh_truth[0].filep)) self.assertFalse(self.config.is_site_enabled(self.vh_truth[1].filep)) self.assertTrue(self.config.is_site_enabled(self.vh_truth[2].filep)) @@ -134,9 +148,9 @@ class TwoVhost80Test(util.NginxTest): self.assertEqual(len(self.config.vhosts), 5) - @mock.patch("letsencrypt.client.nginx.configurator." + @mock.patch("letsencrypt.client.plugins.nginx.configurator." "dvsni.NginxDvsni.perform") - @mock.patch("letsencrypt.client.nginx.configurator." + @mock.patch("letsencrypt.client.plugins.nginx.configurator." "NginxConfigurator.restart") def test_perform(self, mock_restart, mock_dvsni_perform): # Only tests functionality specific to configurator.perform @@ -166,7 +180,7 @@ class TwoVhost80Test(util.NginxTest): self.assertEqual(mock_restart.call_count, 1) - @mock.patch("letsencrypt.client.nginx.configurator." + @mock.patch("letsencrypt.client.plugins.nginx.configurator." "subprocess.Popen") def test_get_version(self, mock_popen): mock_popen().communicate.return_value = ( @@ -183,7 +197,7 @@ class TwoVhost80Test(util.NginxTest): errors.LetsEncryptConfiguratorError, self.config.get_version) mock_popen().communicate.return_value = ( - "Server Version: Nginx/2.3\n Nginx/2.4.7", "") + "Server Version: Nginx/2.3{0} Nginx/2.4.7".format(os.linesep), "") self.assertRaises( errors.LetsEncryptConfiguratorError, self.config.get_version) @@ -192,5 +206,5 @@ class TwoVhost80Test(util.NginxTest): errors.LetsEncryptConfiguratorError, self.config.get_version) -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/letsencrypt/client/plugins/nginx/tests/dvsni_test.py b/letsencrypt/client/plugins/nginx/tests/dvsni_test.py deleted file mode 100644 index 869b5e806..000000000 --- a/letsencrypt/client/plugins/nginx/tests/dvsni_test.py +++ /dev/null @@ -1,170 +0,0 @@ -"""Test for letsencrypt.client.nginx.dvsni.""" -import pkg_resources -import unittest -import shutil - -import mock - -from letsencrypt.acme import challenges - -from letsencrypt.client import achallenges -from letsencrypt.client import le_util - -from letsencrypt.client.nginx.obj import Addr - -from letsencrypt.client.tests.nginx import util - - -class DvsniPerformTest(util.NginxTest): - """Test the NginxDVSNI challenge.""" - - def setUp(self): - super(DvsniPerformTest, self).setUp() - - with mock.patch("letsencrypt.client.nginx.configurator." - "mod_loaded") as mock_load: - mock_load.return_value = True - config = util.get_nginx_configurator( - self.config_path, self.config_dir, self.work_dir, - self.ssl_options) - - from letsencrypt.client.nginx import dvsni - self.sni = dvsni.NginxDvsni(config) - - rsa256_file = pkg_resources.resource_filename( - "letsencrypt.client.tests", 'testdata/rsa256_key.pem') - rsa256_pem = pkg_resources.resource_string( - "letsencrypt.client.tests", 'testdata/rsa256_key.pem') - - auth_key = le_util.Key(rsa256_file, rsa256_pem) - self.achalls = [ - achallenges.DVSNI( - chall=challenges.DVSNI( - r="\x8c\x8a\xbf_-f\\cw\xee\xd6\xf8/\xa5\xe3\xfd\xeb9\xf1" - "\xf5\xb9\xefVM\xc9w\xa4u\x9c\xe1\x87\xb4", - nonce="7\xbc^\xb7]>\x00\xa1\x9bOcU\x84^Z\x18", - ), domain="encryption-example.demo", key=auth_key), - achallenges.DVSNI( - chall=challenges.DVSNI( - r="\xba\xa9\xda? Date: Mon, 6 Apr 2015 14:22:27 -0700 Subject: [PATCH 07/38] Mark semiprivate methods in configurator --- .../client/plugins/nginx/configurator.py | 77 ++++++++-------- letsencrypt/client/plugins/nginx/parser.py | 92 ++++--------------- 2 files changed, 58 insertions(+), 111 deletions(-) diff --git a/letsencrypt/client/plugins/nginx/configurator.py b/letsencrypt/client/plugins/nginx/configurator.py index 624d24ca9..64d07d717 100644 --- a/letsencrypt/client/plugins/nginx/configurator.py +++ b/letsencrypt/client/plugins/nginx/configurator.py @@ -35,6 +35,12 @@ class NginxConfigurator(object): :ivar parser: Handles low level parsing :type parser: :class:`~letsencrypt.client.plugins.nginx.parser` + :ivar set save_files: Files that need to be saved + :ivar str save_notes: Human-readable config change notes + + :ivar reverter: saves and reverts checkpoints + :type reverter: :class:`letsencrypt.client.reverter.Reverter` + :ivar tup version: version of Nginx :ivar list vhosts: All vhosts found in the configuration (:class:`list` of @@ -55,11 +61,14 @@ class NginxConfigurator(object): """ self.config = config - self.save_notes = "" # Verify that all directories and files exist with proper permissions if os.geteuid() == 0: - self.verify_setup() + self._verify_setup() + + # Files to save + self.save_files = set() + self.save_notes = "" # Add name_server association dict self.assoc = dict() @@ -76,6 +85,7 @@ class NginxConfigurator(object): self.reverter = reverter.Reverter(config) self.reverter.recovery_routine() + # This is called in determine_authenticator and determine_installer def prepare(self): """Prepare the authenticator/installer.""" self.parser = parser.NginxParser( @@ -84,13 +94,14 @@ class NginxConfigurator(object): # Set Version if self.version is None: - self.version = self.get_version() + self.version = self._get_version() # Get all of the available vhosts - self.vhosts = self.get_virtual_hosts() + self.vhosts = self._get_vhosts() temp_install(self.config.nginx_mod_ssl_conf) + # Entry point in main.py for installing cert def deploy_cert(self, domain, cert, key, cert_chain=None): """Deploys certificate to specified virtual host. @@ -154,7 +165,7 @@ class NginxConfigurator(object): # Make sure vhost is enabled if not vhost.enabled: - self.enable_site(vhost) + self._enable_site(vhost) ####################### # Vhost parsing methods @@ -172,7 +183,6 @@ class NginxConfigurator(object): """ # Allows for domain names to be associated with a virtual host - # Client isn't using create_dn_server_assoc(self, dn, vh) yet if target_name in self.assoc: return self.assoc[target_name] # Check for servernames/aliases for ssl hosts @@ -191,7 +201,7 @@ class NginxConfigurator(object): # Check for non ssl vhosts with servernames/aliases == "name" for vhost in self.vhosts: if not vhost.ssl and target_name in vhost.names: - vhost = self.make_vhost_ssl(vhost) + vhost = self._make_vhost_ssl(vhost) self.assoc[target_name] = vhost return vhost @@ -201,19 +211,6 @@ class NginxConfigurator(object): return vhost return None - def create_dn_server_assoc(self, domain, vhost): - """Create an association between a domain name and virtual host. - - Helps to choose an appropriate vhost - - :param str domain: domain name to associate - - :param vhost: virtual host to associate with domain - :type vhost: :class:`~letsencrypt.client.plugins.nginx.obj.VirtualHost` - - """ - self.assoc[domain] = vhost - def get_all_names(self): """Returns all names found in the Nginx Configuration. @@ -243,7 +240,7 @@ class NginxConfigurator(object): return all_names # TODO: make "sites-available" a configurable directory - def get_virtual_hosts(self): + def _get_vhosts(self): """Returns list of virtual hosts found in the Nginx configuration. :returns: List of @@ -261,7 +258,7 @@ class NginxConfigurator(object): return vhs - def make_vhost_ssl(self, nonssl_vhost): # pylint: disable=too-many-locals + 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 @@ -296,7 +293,7 @@ class NginxConfigurator(object): new_file.write(line) new_file.write("\n") except IOError: - logging.fatal("Error writing/reading to file in make_vhost_ssl") + logging.fatal("Error writing/reading to file in _make_vhost_ssl") sys.exit(49) self.aug.load() @@ -356,12 +353,13 @@ class NginxConfigurator(object): for vhost in self.vhosts: if vhost.ssl: # TODO: get the cert, key, and conf file paths + pass return c_k - ##################### - # enhancement methods - ##################### + ################################## + # enhancement methods (IInstaller) + ################################## def supported_enhancements(self): # pylint: disable=no-self-use """Returns currently supported enhancements.""" return [] @@ -386,10 +384,10 @@ class NginxConfigurator(object): except errors.LetsEncryptConfiguratorError: logging.warn("Failed %s for %s", enhancement, domain) - ######################### - # Nginx server management - ######################### - def is_site_enabled(self, avail_fp): + ###################################### + # Nginx server management (IInstaller) + ###################################### + def _is_site_enabled(self, avail_fp): """Checks to see if the given site is enabled. .. todo:: fix hardcoded sites-enabled, check os.path.samefile @@ -407,7 +405,7 @@ class NginxConfigurator(object): return False - def enable_site(self, vhost): + def _enable_site(self, vhost): """Enables an available site, Nginx restart required. .. todo:: This function should number subdomains before the domain vhost @@ -421,7 +419,7 @@ class NginxConfigurator(object): :rtype: bool """ - if self.is_site_enabled(vhost.filep): + if self._is_site_enabled(vhost.filep): return True if "/sites-available/" in vhost.filep: @@ -470,7 +468,7 @@ class NginxConfigurator(object): return True - def verify_setup(self): + def _verify_setup(self): """Verify the setup to ensure safe operating environment. Make sure that files/directories are setup with appropriate permissions @@ -483,7 +481,7 @@ class NginxConfigurator(object): le_util.make_or_verify_dir(self.config.work_dir, 0o755, uid) le_util.make_or_verify_dir(self.config.backup_dir, 0o755, uid) - def get_version(self): + def _get_version(self): """Return version of Nginx Server. Version is returned as tuple. (ie. 2.4.7 = (2, 4, 7)) @@ -539,9 +537,9 @@ class NginxConfigurator(object): version=".".join(str(i) for i in self.version)) ) - ###################################### - # Wrapper functions for Reverter class - ###################################### + ################################################### + # Wrapper functions for Reverter class (IInstaller) + ################################################### def save(self, title=None, temporary=False): """Saves all changes to the configuration files. @@ -571,6 +569,7 @@ class NginxConfigurator(object): os.rename(f + '.le', f) else: logging.warn("Expected file %s to exist", tmpfile) + self.save_files.remove(f) if title and not temporary: self.reverter.finalize_checkpoint(title) @@ -602,12 +601,13 @@ class NginxConfigurator(object): self.reverter.view_config_changes() ########################################################################### - # Challenges Section + # Challenges Section for IAuthenticator ########################################################################### def get_chall_pref(self, unused_domain): # pylint: disable=no-self-use """Return list of challenge preferences.""" return [challenges.DVSNI] + # Entry point in main.py for performing challenges def perform(self, achalls): """Perform the configuration related challenge. @@ -640,6 +640,7 @@ class NginxConfigurator(object): return responses + # called after challenges are performed def cleanup(self, achalls): """Revert all challenges.""" self._chall_out -= len(achalls) diff --git a/letsencrypt/client/plugins/nginx/parser.py b/letsencrypt/client/plugins/nginx/parser.py index 0f95c056c..dfb091881 100644 --- a/letsencrypt/client/plugins/nginx/parser.py +++ b/letsencrypt/client/plugins/nginx/parser.py @@ -1,8 +1,12 @@ """NginxParser is a member object of the NginxConfigurator class.""" +import glob +import logging import os import re +import pyparsing from letsencrypt.client import errors +from letsencrypt.client.plugins.nginx.nginxparser import dump, load class NginxParser(object): @@ -10,22 +14,19 @@ class NginxParser(object): :ivar str root: Normalized abosulte path to the server root directory. Without trailing slash. + :ivar dict parsed: Mapping of file paths to parsed trees """ - def __init__(self, aug, root, ssl_options): - # Find configuration root and make sure augeas can parse it. - self.aug = aug + def __init__(self, root, ssl_options): + self.parsed = {} self.root = os.path.abspath(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() + self._parse_file(os.path.join(self.root, "sites-available") + "/*.conf") def add_dir_to_ifmodssl(self, aug_conf_path, directive, val): """Adds directive and value to IfMod ssl block. @@ -246,24 +247,19 @@ class NginxParser(object): return regex def _parse_file(self, filepath): - """Parse file with Augeas - - Checks to see if file_path is parsed by Augeas - If filepath isn't parsed, the file is added and Augeas is reloaded + """Parse file :param str filepath: Nginx 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']" % filepath) - if not inc_test: - # Load up files - # This doesn't seem to work on TravisCI - # self.aug.add_transform("Httpd.lns", [filepath]) - self._add_httpd_transform(filepath) - self.aug.load() + files = glob.glob(filepath) + for f in files: + try: + self.parsed[f] = load(open(f)) + except IOError: + logging.warn("Could not parse file: %s" % f) + except pyparsing.ParseException: + logging.warn("Could not parse file: %s" % f) def _add_httpd_transform(self, incl): """Add a transform to Augeas. @@ -286,38 +282,6 @@ class NginxParser(object): self.aug.set("/augeas/load/Httpd/lens", "Httpd.lns") self.aug.set("/augeas/load/Httpd/incl", incl) - 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, excluded in enumerate(excl, 1): - self.aug.set("/augeas/load/Httpd/excl[%d]" % i, excluded) - - self.aug.load() - def _set_locations(self, ssl_options): """Set default location for directives. @@ -326,7 +290,7 @@ class NginxParser(object): """ root = self._find_config_root() - default = self._set_user_config_file(root) + default = os.path.join(self.root, 'nginx.conf') temp = os.path.join(self.root, "ports.conf") if os.path.isfile(temp): @@ -341,7 +305,7 @@ class NginxParser(object): def _find_config_root(self): """Find the Nginx Configuration Root file.""" - location = ["nginx2.conf", "httpd.conf"] + location = ['nginx.conf'] for name in location: if os.path.isfile(os.path.join(self.root, name)): @@ -350,24 +314,6 @@ class NginxParser(object): raise errors.LetsEncryptNoInstallationError( "Could not find configuration root") - def _set_user_config_file(self, root): - """Set the appropriate user configuration file - - .. todo:: This will have to be updated for other distros versions - - :param str root: pathname which contains the user config - - """ - # Basic check to see if httpd.conf exists and - # in hierarchy via direct include - # httpd.conf was very common as a user file in Nginx 2.2 - if (os.path.isfile(os.path.join(self.root, 'httpd.conf')) and - self.find_dir( - case_i("Include"), case_i("httpd.conf"), root)): - return os.path.join(self.root, 'httpd.conf') - else: - return os.path.join(self.root, 'nginx2.conf') - def case_i(string): """Returns case insensitive regex. From 13232452f8041603213f2dde8daba2547446227b Mon Sep 17 00:00:00 2001 From: yan Date: Mon, 6 Apr 2015 18:00:21 -0700 Subject: [PATCH 08/38] Add recursive 'include' parsing to nginx parser --- .../client/plugins/nginx/configurator.py | 21 +--- letsencrypt/client/plugins/nginx/dvsni.py | 4 +- .../client/plugins/nginx/nginxparser.py | 2 +- letsencrypt/client/plugins/nginx/obj.py | 9 +- letsencrypt/client/plugins/nginx/parser.py | 113 ++++++++++++++++-- .../plugins/nginx/tests/testdata/nginx.conf | 6 +- .../nginx/tests/testdata/nginx.new.conf | 58 --------- 7 files changed, 117 insertions(+), 96 deletions(-) diff --git a/letsencrypt/client/plugins/nginx/configurator.py b/letsencrypt/client/plugins/nginx/configurator.py index 64d07d717..a15d42eb2 100644 --- a/letsencrypt/client/plugins/nginx/configurator.py +++ b/letsencrypt/client/plugins/nginx/configurator.py @@ -97,7 +97,7 @@ class NginxConfigurator(object): self.version = self._get_version() # Get all of the available vhosts - self.vhosts = self._get_vhosts() + self.vhosts = self.parser._get_vhosts() temp_install(self.config.nginx_mod_ssl_conf) @@ -239,25 +239,6 @@ class NginxConfigurator(object): return all_names - # TODO: make "sites-available" a configurable directory - def _get_vhosts(self): - """Returns list of virtual hosts found in the Nginx configuration. - - :returns: List of - :class:`~letsencrypt.client.plugins.nginx.obj.VirtualHost` objects - found in configuration - :rtype: list - - """ - # Search sites-available/, conf.d/, nginx.conf for possible vhosts - paths = self.parser.get_conf_files() - vhs = [] - - for path in paths: - vhs.append(self.parser.get_vhosts(path)) - - return vhs - def _make_vhost_ssl(self, nonssl_vhost): # pylint: disable=too-many-locals """Makes an ssl_vhost version of a nonssl_vhost. diff --git a/letsencrypt/client/plugins/nginx/dvsni.py b/letsencrypt/client/plugins/nginx/dvsni.py index 960352831..c20ce1c0e 100644 --- a/letsencrypt/client/plugins/nginx/dvsni.py +++ b/letsencrypt/client/plugins/nginx/dvsni.py @@ -42,6 +42,7 @@ class NginxDvsni(object): """ + def __init__(self, configurator): self.configurator = configurator self.achalls = [] @@ -83,9 +84,6 @@ class NginxDvsni(object): logging.error("Please specify servernames in the Nginx config") return None - # 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]) diff --git a/letsencrypt/client/plugins/nginx/nginxparser.py b/letsencrypt/client/plugins/nginx/nginxparser.py index 3d01d7ad4..2182ef6a7 100644 --- a/letsencrypt/client/plugins/nginx/nginxparser.py +++ b/letsencrypt/client/plugins/nginx/nginxparser.py @@ -1,4 +1,4 @@ -"""An nginx config parser based on pyparsing.""" +"""Very low-level nginx config parser based on pyparsing.""" import string from pyparsing import ( diff --git a/letsencrypt/client/plugins/nginx/obj.py b/letsencrypt/client/plugins/nginx/obj.py index 69e0d6b20..85a7fa003 100644 --- a/letsencrypt/client/plugins/nginx/obj.py +++ b/letsencrypt/client/plugins/nginx/obj.py @@ -47,7 +47,6 @@ class VirtualHost(object): # pylint: disable=too-few-public-methods """Represents an Nginx Virtualhost. :ivar str filep: file path of VH - :ivar str path: Augeas path to virtual host :ivar set addrs: Virtual Host addresses (:class:`set` of :class:`Addr`) :ivar set names: Server names/aliases of vhost (:class:`list` of :class:`str`) @@ -57,11 +56,10 @@ class VirtualHost(object): # pylint: disable=too-few-public-methods """ - def __init__(self, filep, path, addrs, ssl, enabled, names=None): + def __init__(self, filep, addrs, ssl, enabled, names=None): # pylint: disable=too-many-arguments """Initialize a VH.""" self.filep = filep - self.path = path self.addrs = addrs self.names = set() if names is None else set(names) self.ssl = ssl @@ -74,16 +72,15 @@ class VirtualHost(object): # pylint: disable=too-few-public-methods def __str__(self): addr_str = ", ".join(str(addr) for addr in self.addrs) return ("file: %s\n" - "vh_path: %s\n" "addrs: %s\n" "names: %s\n" "ssl: %s\n" - "enabled: %s" % (self.filep, self.path, addr_str, + "enabled: %s" % (self.filep, addr_str, self.names, self.ssl, self.enabled)) def __eq__(self, other): if isinstance(other, self.__class__): - return (self.filep == other.filep and self.path == other.path and + return (self.filep == other.filep and self.addrs == other.addrs and self.names == other.names and self.ssl == other.ssl and self.enabled == other.enabled) diff --git a/letsencrypt/client/plugins/nginx/parser.py b/letsencrypt/client/plugins/nginx/parser.py index dfb091881..6fc5bef53 100644 --- a/letsencrypt/client/plugins/nginx/parser.py +++ b/letsencrypt/client/plugins/nginx/parser.py @@ -6,6 +6,7 @@ import re import pyparsing from letsencrypt.client import errors +from letsencrypt.client.plugins.nginx import obj from letsencrypt.client.plugins.nginx.nginxparser import dump, load @@ -22,11 +23,101 @@ class NginxParser(object): self.parsed = {} self.root = os.path.abspath(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") + "/*.conf") + # Parse nginx.conf and included files. + # TODO: Check sites-available/ as well. For now, the configurator does + # not enable sites from there. + self._parse_recursively(self.loc["root"]) + + def _parse_recursively(self, filepath): + """Parses nginx config files recursively by looking at 'include' + directives inside 'http' and 'server' blocks. Note that this only + reads Nginx files that potentially declare a virtual host. + + .. todo:: Can Nginx 'virtual hosts' be defined somewhere other than in + the server context? + + """ + trees = self._parse_files(filepath) + for tree in trees: + for entry in tree: + if self._is_include_directive(entry): + # Parse the top-level included file + self._parse_recursively(entry[1]) + elif entry[0] == ['http'] or entry[0] == ['server']: + # Look for includes in the top-level 'http'/'server' context + for subentry in entry[1]: + if self._is_include_directive(subentry): + self._parse_recursively(subentry[1]) + elif entry[0] == ['http'] and subentry[0] == ['server']: + # Look for includes in a 'server' context within + # an 'http' context + for server_entry in subentry[1]: + if self._is_include_directive(server_entry): + self._parse_recursively(server_entry[1]) + + def _is_include_directive(self, entry): + """Checks if an nginx parsed entry is an 'include' directive. + + :param list entry: the parsed entry + :returns: Whether it's an 'include' directive + :rtype: bool + + """ + return (entry[0] == 'include' and len(entry) == 2 and + type(entry[1]) == str) + + def _get_names(self, entry): + """Gets server names from nginx parsed entry. + + :param list entry: the parsed entry + :returns: Set of server names + :rtype: set + + """ + return set() + + def _get_addrs(self, entry): + """Gets addresses from nginx parsed entry. + + :param list entry: the parsed entry + :returns: Set of + :class:`~letsencrypt.client.plugins.nginx.obj.Addr` objects + :rtype: set + + """ + return set() + + def _get_ssl(self, entry): + """Gets whether the nginx parsed entry is SSL-enabled. + + :param list entry: the parsed entry + :returns: Whether it's SSL-enabled + :rtype: bool + + """ + return False + + def get_vhosts(self): + """Gets list of all 'virtual hosts' found in Nginx configuration. + Technically this is a misnomer because Nginx does not have virtual + hosts, it has 'server blocks'. + + :returns: List of + :class:`~letsencrypt.client.plugins.nginx.obj.VirtualHost` objects + found in configuration + :rtype: list + + """ + enabled = True # We only look at enabled vhosts for now + vhosts = [] + for filename, tree in self.parsed: + vhost = obj.VirtulHost(filename, + self._get_addrs(tree), + self._get_ssl(tree), + enabled, + self._get_names(tree)) + vhosts.append(vhost) def add_dir_to_ifmodssl(self, aug_conf_path, directive, val): """Adds directive and value to IfMod ssl block. @@ -200,7 +291,7 @@ class NginxParser(object): # TODO: Test if Nginx allows ../ or ~/ for Includes # Attempts to add a transform to the file if one does not already exist - self._parse_file(arg) + self._parse_files(arg) # Argument represents an fnmatch regular expression, convert it # Split up the path and convert each into an Augeas accepted regex @@ -246,20 +337,28 @@ class NginxParser(object): regex = regex + letter return regex - def _parse_file(self, filepath): + def _parse_files(self, filepath): """Parse file :param str filepath: Nginx config file path + :returns: list of parsed tree structures + :rtype: list """ files = glob.glob(filepath) + trees = [] for f in files: + if f in self.parsed: + continue try: - self.parsed[f] = load(open(f)) + parsed = load(open(f)) + self.parsed[f] = parsed + trees.append(parsed) except IOError: logging.warn("Could not parse file: %s" % f) except pyparsing.ParseException: logging.warn("Could not parse file: %s" % f) + return trees def _add_httpd_transform(self, incl): """Add a transform to Augeas. diff --git a/letsencrypt/client/plugins/nginx/tests/testdata/nginx.conf b/letsencrypt/client/plugins/nginx/tests/testdata/nginx.conf index 057edba6f..67566604e 100644 --- a/letsencrypt/client/plugins/nginx/tests/testdata/nginx.conf +++ b/letsencrypt/client/plugins/nginx/tests/testdata/nginx.conf @@ -14,6 +14,7 @@ events { worker_connections 1024; } +include foo.conf http { include mime.types; @@ -84,7 +85,7 @@ http { server { listen 8000; listen somename:8080; - server_name somename alias another.alias; + include server.conf; location / { root html; @@ -114,4 +115,7 @@ http { # } #} + include conf.d/test.conf; + include sites-enabled/*; + } diff --git a/letsencrypt/client/plugins/nginx/tests/testdata/nginx.new.conf b/letsencrypt/client/plugins/nginx/tests/testdata/nginx.new.conf index 610ded391..210861593 100644 --- a/letsencrypt/client/plugins/nginx/tests/testdata/nginx.new.conf +++ b/letsencrypt/client/plugins/nginx/tests/testdata/nginx.new.conf @@ -6,64 +6,6 @@ error_log logs/error.log info; pid logs/nginx.pid; events { worker_connections 1024; -} -http { - include mime.types; - default_type application/octet-stream; - log_format main '$remote_addr - $remote_user [$time_local] "$request" ' - '$status $body_bytes_sent "$http_referer" ' - '"$http_user_agent" "$http_x_forwarded_for"'; - access_log logs/access.log main; - sendfile on; - tcp_nopush on; - keepalive_timeout 0; - keepalive_timeout 65; - gzip on; - - server { - listen 8080; - server_name localhost; - charset koi8-r; - access_log logs/host.access.log main; - - location / { - root html; - index index.html index.htm; - } - error_page 404 /404.html; - error_page 500 502 503 504 /50x.html; - - location = /50x.html { - root html; - } - - location ~ \.php$ { - proxy_pass http://127.0.0.1; - } - - location ~ \.php$ { - root html; - fastcgi_pass 127.0.0.1:9000; - fastcgi_index index.php; - fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name; - include fastcgi_params; - } - - location ~ /\.ht { - deny all; - } - } - - server { - listen 8000; - listen somename:8080; - server_name somename alias another.alias; - - location / { - root html; - index index.html index.htm; - } - } server { listen 443 ssl; From b245394355bcc29ff7998bd24952527ecc1bb7b2 Mon Sep 17 00:00:00 2001 From: yan Date: Mon, 6 Apr 2015 18:00:38 -0700 Subject: [PATCH 09/38] Add test server.conf file --- letsencrypt/client/plugins/nginx/parser.py | 1 + .../plugins/nginx/tests/testdata/nginx.conf | 2 +- .../nginx/tests/testdata/nginx.new.conf | 61 +++++++++++++++++++ .../plugins/nginx/tests/testdata/server.conf | 1 + 4 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 letsencrypt/client/plugins/nginx/tests/testdata/server.conf diff --git a/letsencrypt/client/plugins/nginx/parser.py b/letsencrypt/client/plugins/nginx/parser.py index 6fc5bef53..f8a21d72b 100644 --- a/letsencrypt/client/plugins/nginx/parser.py +++ b/letsencrypt/client/plugins/nginx/parser.py @@ -96,6 +96,7 @@ class NginxParser(object): :rtype: bool """ + # Look for a server block that contains 'listen [port] ssl' return False def get_vhosts(self): diff --git a/letsencrypt/client/plugins/nginx/tests/testdata/nginx.conf b/letsencrypt/client/plugins/nginx/tests/testdata/nginx.conf index 67566604e..ce8e525ef 100644 --- a/letsencrypt/client/plugins/nginx/tests/testdata/nginx.conf +++ b/letsencrypt/client/plugins/nginx/tests/testdata/nginx.conf @@ -14,7 +14,7 @@ events { worker_connections 1024; } -include foo.conf +include foo.conf; http { include mime.types; diff --git a/letsencrypt/client/plugins/nginx/tests/testdata/nginx.new.conf b/letsencrypt/client/plugins/nginx/tests/testdata/nginx.new.conf index 210861593..e53ed29c9 100644 --- a/letsencrypt/client/plugins/nginx/tests/testdata/nginx.new.conf +++ b/letsencrypt/client/plugins/nginx/tests/testdata/nginx.new.conf @@ -6,6 +6,67 @@ error_log logs/error.log info; pid logs/nginx.pid; events { worker_connections 1024; +} +include foo.conf; +http { + include mime.types; + default_type application/octet-stream; + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + access_log logs/access.log main; + sendfile on; + tcp_nopush on; + keepalive_timeout 0; + keepalive_timeout 65; + gzip on; + + server { + listen 8080; + server_name localhost; + charset koi8-r; + access_log logs/host.access.log main; + + location / { + root html; + index index.html index.htm; + } + error_page 404 /404.html; + error_page 500 502 503 504 /50x.html; + + location = /50x.html { + root html; + } + + location ~ \.php$ { + proxy_pass http://127.0.0.1; + } + + location ~ \.php$ { + root html; + fastcgi_pass 127.0.0.1:9000; + fastcgi_index index.php; + fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name; + include fastcgi_params; + } + + location ~ /\.ht { + deny all; + } + } + + server { + listen 8000; + listen somename:8080; + include server.conf; + + location / { + root html; + index index.html index.htm; + } + } + include conf.d/test.conf; + include sites-enabled/*; server { listen 443 ssl; diff --git a/letsencrypt/client/plugins/nginx/tests/testdata/server.conf b/letsencrypt/client/plugins/nginx/tests/testdata/server.conf new file mode 100644 index 000000000..5fc4c8b24 --- /dev/null +++ b/letsencrypt/client/plugins/nginx/tests/testdata/server.conf @@ -0,0 +1 @@ +server_name somename alias another.alias; From eaef4065e31a1fd303c9517ab11fffff185afbc7 Mon Sep 17 00:00:00 2001 From: yan Date: Tue, 7 Apr 2015 11:20:34 -0700 Subject: [PATCH 10/38] Rename NginxParser to RawNginxParser --- .../client/plugins/nginx/nginxparser.py | 8 ++++---- .../plugins/nginx/tests/nginxparser_test.py | 18 +++++++++--------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/letsencrypt/client/plugins/nginx/nginxparser.py b/letsencrypt/client/plugins/nginx/nginxparser.py index 2182ef6a7..c825fbb31 100644 --- a/letsencrypt/client/plugins/nginx/nginxparser.py +++ b/letsencrypt/client/plugins/nginx/nginxparser.py @@ -6,7 +6,7 @@ from pyparsing import ( Optional, OneOrMore, ZeroOrMore, pythonStyleComment) -class NginxParser(object): +class RawNginxParser(object): """ A class that parses nginx configuration with pyparsing """ @@ -50,7 +50,7 @@ class NginxParser(object): return self.parse().asList() -class NginxDumper(object): +class RawNginxDumper(object): """ A class that dumps nginx configuration from the provided tree. """ @@ -93,7 +93,7 @@ class NginxDumper(object): # (like pyyaml, picker or json) def loads(source): - return NginxParser(source).as_list() + return RawNginxParser(source).as_list() def load(_file): @@ -101,7 +101,7 @@ def load(_file): def dumps(blocks, indentation=4): - return NginxDumper(blocks, indentation).as_string() + return RawNginxDumper(blocks, indentation).as_string() def dump(blocks, _file, indentation=4): diff --git a/letsencrypt/client/plugins/nginx/tests/nginxparser_test.py b/letsencrypt/client/plugins/nginx/tests/nginxparser_test.py index 6c27ef5e1..fe5f884d3 100644 --- a/letsencrypt/client/plugins/nginx/tests/nginxparser_test.py +++ b/letsencrypt/client/plugins/nginx/tests/nginxparser_test.py @@ -1,7 +1,7 @@ import operator import unittest -from letsencrypt.client.plugins.nginx.nginxparser import (NginxParser, +from letsencrypt.client.plugins.nginx.nginxparser import (RawNginxParser, load, dumps, dump) from letsencrypt.client.plugins.nginx.tests import util @@ -9,25 +9,25 @@ from letsencrypt.client.plugins.nginx.tests import util first = operator.itemgetter(0) -class TestNginxParser(unittest.TestCase): +class TestRawNginxParser(unittest.TestCase): def test_assignments(self): - parsed = NginxParser.assignment.parseString('root /test;').asList() + parsed = RawNginxParser.assignment.parseString('root /test;').asList() self.assertEqual(parsed, ['root', '/test']) - parsed = NginxParser.assignment.parseString('root /test;' - 'foo bar;').asList() + parsed = RawNginxParser.assignment.parseString('root /test;' + 'foo bar;').asList() self.assertEqual(parsed, ['root', '/test'], ['foo', 'bar']) def test_blocks(self): - parsed = NginxParser.block.parseString('foo {}').asList() + parsed = RawNginxParser.block.parseString('foo {}').asList() self.assertEqual(parsed, [[['foo'], []]]) - parsed = NginxParser.block.parseString('location /foo{}').asList() + parsed = RawNginxParser.block.parseString('location /foo{}').asList() self.assertEqual(parsed, [[['location', '/foo'], []]]) - parsed = NginxParser.block.parseString('foo { bar foo; }').asList() + parsed = RawNginxParser.block.parseString('foo { bar foo; }').asList() self.assertEqual(parsed, [[['foo'], [['bar', 'foo']]]]) def test_nested_blocks(self): - parsed = NginxParser.block.parseString('foo { bar {} }').asList() + parsed = RawNginxParser.block.parseString('foo { bar {} }').asList() block, content = first(parsed) self.assertEqual(first(content), [['bar'], []]) From 4f3bf3d720c737c6eaa6fad4683d7a0f469948b0 Mon Sep 17 00:00:00 2001 From: yan Date: Tue, 7 Apr 2015 14:57:37 -0700 Subject: [PATCH 11/38] Add test for recursive file parsing --- letsencrypt/client/plugins/nginx/parser.py | 23 +++- .../client/plugins/nginx/tests/parser_test.py | 123 ++++++++++++++++++ .../tests/testdata/sites-enabled/default | 9 ++ .../tests/testdata/sites-enabled/example.com | 4 + .../client/plugins/nginx/tests/util.py | 10 +- 5 files changed, 161 insertions(+), 8 deletions(-) create mode 100644 letsencrypt/client/plugins/nginx/tests/parser_test.py create mode 100644 letsencrypt/client/plugins/nginx/tests/testdata/sites-enabled/default create mode 100644 letsencrypt/client/plugins/nginx/tests/testdata/sites-enabled/example.com diff --git a/letsencrypt/client/plugins/nginx/parser.py b/letsencrypt/client/plugins/nginx/parser.py index f8a21d72b..6fd6f381d 100644 --- a/letsencrypt/client/plugins/nginx/parser.py +++ b/letsencrypt/client/plugins/nginx/parser.py @@ -37,7 +37,10 @@ class NginxParser(object): .. todo:: Can Nginx 'virtual hosts' be defined somewhere other than in the server context? + :param str filepath: The path to the files to parse, as a glob + """ + filepath = self.abs_path(filepath) trees = self._parse_files(filepath) for tree in trees: for entry in tree: @@ -56,6 +59,20 @@ class NginxParser(object): if self._is_include_directive(server_entry): self._parse_recursively(server_entry[1]) + def abs_path(self, path): + """Converts a relative path to an absolute path relative to the root. + Does nothing for paths that are already absolute. + + :param str path: The path + :returns: The absolute path + :rtype str + + """ + if not os.path.isabs(path): + return os.path.join(self.root, path) + else: + return path + def _is_include_directive(self, entry): """Checks if an nginx parsed entry is an 'include' directive. @@ -339,7 +356,7 @@ class NginxParser(object): return regex def _parse_files(self, filepath): - """Parse file + """Parse files from a glob :param str filepath: Nginx config file path :returns: list of parsed tree structures @@ -356,7 +373,7 @@ class NginxParser(object): self.parsed[f] = parsed trees.append(parsed) except IOError: - logging.warn("Could not parse file: %s" % f) + logging.warn("Could not open file: %s" % f) except pyparsing.ParseException: logging.warn("Could not parse file: %s" % f) return trees @@ -390,7 +407,7 @@ class NginxParser(object): """ root = self._find_config_root() - default = os.path.join(self.root, 'nginx.conf') + default = root temp = os.path.join(self.root, "ports.conf") if os.path.isfile(temp): diff --git a/letsencrypt/client/plugins/nginx/tests/parser_test.py b/letsencrypt/client/plugins/nginx/tests/parser_test.py new file mode 100644 index 000000000..c3c809521 --- /dev/null +++ b/letsencrypt/client/plugins/nginx/tests/parser_test.py @@ -0,0 +1,123 @@ +"""Tests for letsencrypt.client.plugins.nginx.parser.""" +import os +import shutil +import sys +import unittest + +import mock +import zope.component + +from letsencrypt.client import errors +from letsencrypt.client.display import util as display_util + +from letsencrypt.client.plugins.nginx.parser import NginxParser +from letsencrypt.client.plugins.nginx.tests import util + + +class NginxParserTest(util.NginxTest): + """Nginx Parser Test.""" + + def setUp(self): + super(NginxParserTest, self).setUp() + + self.maxDiff = None + zope.component.provideUtility(display_util.FileDisplay(sys.stdout)) + + def tearDown(self): + shutil.rmtree(self.temp_dir) + shutil.rmtree(self.config_dir) + shutil.rmtree(self.work_dir) + + def test_root_normalized(self): + path = os.path.join(self.temp_dir, "debian_nginx_2_4/////" + "two_vhost_80/../../testdata") + parser = NginxParser(path, None) + self.assertEqual(parser.root, self.config_path) + + def test_root_absolute(self): + parser = NginxParser(os.path.relpath(self.config_path), None) + self.assertEqual(parser.root, self.config_path) + + def test_root_no_trailing_slash(self): + parser = NginxParser(self.config_path + os.path.sep, None) + self.assertEqual(parser.root, self.config_path) + + def test_parse(self): + """Test recursive conf file parsing. + + """ + self.parser = NginxParser(self.config_path, self.ssl_options) + self.assertEqual(set(map(self.parser.abs_path, + ['foo.conf', 'nginx.conf', 'server.conf', + 'sites-enabled/default', + 'sites-enabled/example.com'])), + set(self.parser.parsed.keys())) + self.assertEqual([['server_name', 'somename alias another.alias']], + self.parser.parsed[self.parser.abs_path( + 'server.conf')]) + self.assertEqual([[['server'], [['listen', '9000'], + ['server_name', 'example.com']]]], + self.parser.parsed[self.parser.abs_path( + 'sites-enabled/example.com')]) + +# def test_find_dir(self): +# from letsencrypt.client.plugins.nginx.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_add_dir(self): +# aug_default = "/files" + self.parser.loc["default"] +# self.parser.add_dir(aug_default, "AddDirective", "test") +# +# self.assertTrue( +# self.parser.find_dir("AddDirective", "test", aug_default)) +# +# self.parser.add_dir(aug_default, "AddList", ["1", "2", "3", "4"]) +# matches = self.parser.find_dir("AddList", None, aug_default) +# for i, match in enumerate(matches): +# self.assertEqual(self.parser.aug.get(match), str(i + 1)) +# +# def test_add_dir_to_ifmodssl(self): +# """test add_dir_to_ifmodssl. +# +# Path must be valid before attempting to add to augeas +# +# """ +# from letsencrypt.client.plugins.nginx.parser import get_aug_path +# self.parser.add_dir_to_ifmodssl( +# get_aug_path(self.parser.loc["default"]), +# "FakeDirective", "123") +# +# matches = self.parser.find_dir("FakeDirective", "123") +# +# self.assertEqual(len(matches), 1) +# self.assertTrue("IfModule" in matches[0]) +# +# def test_get_aug_path(self): +# from letsencrypt.client.plugins.nginx.parser import get_aug_path +# self.assertEqual("/files/etc/nginx", get_aug_path("/etc/nginx")) +# +# def test_set_locations(self): +# with mock.patch("letsencrypt.client.plugins.nginx.parser." +# "os.path") as mock_path: +# +# mock_path.isfile.return_value = False +# +# # pylint: disable=protected-access +# self.assertRaises(errors.LetsEncryptConfiguratorError, +# self.parser._set_locations, self.ssl_options) +# +# mock_path.isfile.side_effect = [True, False, False] +# +# # pylint: disable=protected-access +# results = self.parser._set_locations(self.ssl_options) +# +# self.assertEqual(results["default"], results["listen"]) +# self.assertEqual(results["default"], results["name"]) + + +if __name__ == "__main__": + unittest.main() diff --git a/letsencrypt/client/plugins/nginx/tests/testdata/sites-enabled/default b/letsencrypt/client/plugins/nginx/tests/testdata/sites-enabled/default new file mode 100644 index 000000000..29a311cee --- /dev/null +++ b/letsencrypt/client/plugins/nginx/tests/testdata/sites-enabled/default @@ -0,0 +1,9 @@ +server { + listen 1234; + server_name example.org; + + location / { + root html; + index index.html index.htm; + } +} diff --git a/letsencrypt/client/plugins/nginx/tests/testdata/sites-enabled/example.com b/letsencrypt/client/plugins/nginx/tests/testdata/sites-enabled/example.com new file mode 100644 index 000000000..d61f8a698 --- /dev/null +++ b/letsencrypt/client/plugins/nginx/tests/testdata/sites-enabled/example.com @@ -0,0 +1,4 @@ +server { + listen 9000; + server_name example.com; +} diff --git a/letsencrypt/client/plugins/nginx/tests/util.py b/letsencrypt/client/plugins/nginx/tests/util.py index 975360d6c..e8467502e 100644 --- a/letsencrypt/client/plugins/nginx/tests/util.py +++ b/letsencrypt/client/plugins/nginx/tests/util.py @@ -18,12 +18,12 @@ class NginxTest(unittest.TestCase): # pylint: disable=too-few-public-methods super(NginxTest, self).setUp() self.temp_dir, self.config_dir, self.work_dir = dir_setup( - "debian_nginx_2_4/two_vhost_80") + "testdata") self.ssl_options = setup_nginx_ssl_options(self.config_dir) self.config_path = os.path.join( - self.temp_dir, "debian_nginx_2_4/two_vhost_80/nginx2") + self.temp_dir, "testdata") self.rsa256_file = pkg_resources.resource_filename( "letsencrypt.client.tests", "testdata/rsa256_key.pem") @@ -36,14 +36,14 @@ def get_data_filename(filename): "letsencrypt.client.plugins.nginx.tests", "testdata/%s" % filename) -def dir_setup(test_dir="debian_nginx_2_4/two_vhost_80"): +def dir_setup(test_dir="debian_nginx/two_vhost_80"): """Setup the directories necessary for the configurator.""" temp_dir = tempfile.mkdtemp("temp") config_dir = tempfile.mkdtemp("config") work_dir = tempfile.mkdtemp("work") test_configs = pkg_resources.resource_filename( - "letsencrypt.client.plugins.nginx.tests", "testdata/%s" % test_dir) + "letsencrypt.client.plugins.nginx.tests", test_dir) shutil.copytree( test_configs, os.path.join(temp_dir, test_dir), symlinks=True) @@ -54,7 +54,7 @@ def dir_setup(test_dir="debian_nginx_2_4/two_vhost_80"): def setup_nginx_ssl_options(config_dir): """Move the ssl_options into position and return the path.""" option_path = os.path.join(config_dir, "options-ssl.conf") - shutil.copyfile(constants.APACHE_MOD_SSL_CONF, option_path) + shutil.copyfile(constants.NGINX_MOD_SSL_CONF, option_path) return option_path From d8ac31acae49e7c6020376a9f90d2a0f4122ed6d Mon Sep 17 00:00:00 2001 From: yan Date: Tue, 7 Apr 2015 16:22:34 -0700 Subject: [PATCH 12/38] Add method and test for dumping nginx configs --- .../client/plugins/nginx/nginxparser.py | 4 +-- letsencrypt/client/plugins/nginx/parser.py | 28 ++++++++++++--- .../plugins/nginx/tests/nginxparser_test.py | 1 + .../client/plugins/nginx/tests/parser_test.py | 35 ++++++++++++++----- 4 files changed, 53 insertions(+), 15 deletions(-) diff --git a/letsencrypt/client/plugins/nginx/nginxparser.py b/letsencrypt/client/plugins/nginx/nginxparser.py index c825fbb31..8f995cf61 100644 --- a/letsencrypt/client/plugins/nginx/nginxparser.py +++ b/letsencrypt/client/plugins/nginx/nginxparser.py @@ -105,6 +105,4 @@ def dumps(blocks, indentation=4): def dump(blocks, _file, indentation=4): - _file.write(dumps(blocks, indentation)) - _file.close() - return _file + return _file.write(dumps(blocks, indentation)) diff --git a/letsencrypt/client/plugins/nginx/parser.py b/letsencrypt/client/plugins/nginx/parser.py index 6fd6f381d..ad59911ec 100644 --- a/letsencrypt/client/plugins/nginx/parser.py +++ b/letsencrypt/client/plugins/nginx/parser.py @@ -129,13 +129,15 @@ class NginxParser(object): """ enabled = True # We only look at enabled vhosts for now vhosts = [] - for filename, tree in self.parsed: + for filename in self.parsed: + tree = self.parsed[filename] vhost = obj.VirtulHost(filename, self._get_addrs(tree), self._get_ssl(tree), enabled, self._get_names(tree)) vhosts.append(vhost) + return vhosts def add_dir_to_ifmodssl(self, aug_conf_path, directive, val): """Adds directive and value to IfMod ssl block. @@ -369,9 +371,10 @@ class NginxParser(object): if f in self.parsed: continue try: - parsed = load(open(f)) - self.parsed[f] = parsed - trees.append(parsed) + with open(f) as fo: + parsed = load(fo) + self.parsed[f] = parsed + trees.append(parsed) except IOError: logging.warn("Could not open file: %s" % f) except pyparsing.ParseException: @@ -431,6 +434,23 @@ class NginxParser(object): raise errors.LetsEncryptNoInstallationError( "Could not find configuration root") + def filedump(self, ext='tmp'): + """Dumps parsed configurations into files. + + :param str ext: The file extension to use for the dumped files. If + empty, this overrides the existing conf files. + + """ + for filename in self.parsed: + tree = self.parsed[filename] + if ext: + filename = filename + os.path.extsep + ext + try: + with open(filename, 'w') as f: + dump(tree, f) + except IOError: + logging.error("Could not open file for writing: %s" % filename) + def case_i(string): """Returns case insensitive regex. diff --git a/letsencrypt/client/plugins/nginx/tests/nginxparser_test.py b/letsencrypt/client/plugins/nginx/tests/nginxparser_test.py index fe5f884d3..00ea9e6c5 100644 --- a/letsencrypt/client/plugins/nginx/tests/nginxparser_test.py +++ b/letsencrypt/client/plugins/nginx/tests/nginxparser_test.py @@ -95,6 +95,7 @@ class TestRawNginxParser(unittest.TestCase): ['index', 'index.html index.htm']]]]]) f = open(util.get_data_filename('nginx.new.conf'), 'w') dump(parsed, f) + f.close() parsed_new = load(open(util.get_data_filename('nginx.new.conf'))) self.assertEquals(parsed, parsed_new) diff --git a/letsencrypt/client/plugins/nginx/tests/parser_test.py b/letsencrypt/client/plugins/nginx/tests/parser_test.py index c3c809521..4502c5859 100644 --- a/letsencrypt/client/plugins/nginx/tests/parser_test.py +++ b/letsencrypt/client/plugins/nginx/tests/parser_test.py @@ -1,4 +1,5 @@ """Tests for letsencrypt.client.plugins.nginx.parser.""" +import glob import os import shutil import sys @@ -29,8 +30,8 @@ class NginxParserTest(util.NginxTest): shutil.rmtree(self.work_dir) def test_root_normalized(self): - path = os.path.join(self.temp_dir, "debian_nginx_2_4/////" - "two_vhost_80/../../testdata") + path = os.path.join(self.temp_dir, "foo/////" + "bar/../../testdata") parser = NginxParser(path, None) self.assertEqual(parser.root, self.config_path) @@ -46,20 +47,38 @@ class NginxParserTest(util.NginxTest): """Test recursive conf file parsing. """ - self.parser = NginxParser(self.config_path, self.ssl_options) - self.assertEqual(set(map(self.parser.abs_path, + parser = NginxParser(self.config_path, self.ssl_options) + self.assertEqual(set(map(parser.abs_path, ['foo.conf', 'nginx.conf', 'server.conf', 'sites-enabled/default', 'sites-enabled/example.com'])), - set(self.parser.parsed.keys())) + set(parser.parsed.keys())) self.assertEqual([['server_name', 'somename alias another.alias']], - self.parser.parsed[self.parser.abs_path( - 'server.conf')]) + parser.parsed[parser.abs_path('server.conf')]) self.assertEqual([[['server'], [['listen', '9000'], ['server_name', 'example.com']]]], - self.parser.parsed[self.parser.abs_path( + parser.parsed[parser.abs_path( 'sites-enabled/example.com')]) + def test_abs_path(self): + parser = NginxParser(self.config_path, self.ssl_options) + self.assertEqual('/etc/nginx/*', parser.abs_path('/etc/nginx/*')) + self.assertEqual(os.path.join(self.config_path, 'foo/bar/'), + parser.abs_path('foo/bar/')) + + def test_filedump(self): + parser = NginxParser(self.config_path, self.ssl_options) + parser.filedump('test') + # pylint: disable=protected-access + parsed = parser._parse_files(parser.abs_path( + 'sites-enabled/example.com.test')) + self.assertEqual(3, len(glob.glob(parser.abs_path('*.test')))) + self.assertEqual(2, len( + glob.glob(parser.abs_path('sites-enabled/*.test')))) + self.assertEqual([[['server'], [['listen', '9000'], + ['server_name', 'example.com']]]], + parsed[0]) + # def test_find_dir(self): # from letsencrypt.client.plugins.nginx.parser import case_i # test = self.parser.find_dir(case_i("Listen"), "443") From 4f53c7a3c0782d09836dc2272b7eac77811dd747 Mon Sep 17 00:00:00 2001 From: yan Date: Tue, 7 Apr 2015 18:34:34 -0700 Subject: [PATCH 13/38] Define addr object for nginx --- .../client/plugins/nginx/configurator.py | 3 +- letsencrypt/client/plugins/nginx/obj.py | 58 ++++++++++++++++--- letsencrypt/client/plugins/nginx/parser.py | 2 +- 3 files changed, 53 insertions(+), 10 deletions(-) diff --git a/letsencrypt/client/plugins/nginx/configurator.py b/letsencrypt/client/plugins/nginx/configurator.py index a15d42eb2..4b3239538 100644 --- a/letsencrypt/client/plugins/nginx/configurator.py +++ b/letsencrypt/client/plugins/nginx/configurator.py @@ -109,8 +109,7 @@ class NginxConfigurator(object): 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. + to the correct destination. .. todo:: Make sure last directive is changed diff --git a/letsencrypt/client/plugins/nginx/obj.py b/letsencrypt/client/plugins/nginx/obj.py index 85a7fa003..6e20a78b5 100644 --- a/letsencrypt/client/plugins/nginx/obj.py +++ b/letsencrypt/client/plugins/nginx/obj.py @@ -1,21 +1,65 @@ """Module contains classes used by the Nginx Configurator.""" +import re class Addr(object): - r"""Represents an Nginx VirtualHost address. + """Represents an Nginx address, i.e. what comes after the 'listen' + directive. - :param str addr: addr part of vhost address - :param str port: port number or \*, or "" + According to http://nginx.org/en/docs/http/ngx_http_core_module.html#listen, + this may be address[:port], port, or unix:path. The latter is ignored here. + + The default value if no directive is specified is *:80 (superuser) or + *:8000 (otherwise). If no port is specified, the default is 80. If no + address is specified, listen on all addresses. + + :param str addr: addr part of vhost address, may be hostname, IPv4, IPv6, + "", or "*" + :param str port: port number or "*" or "" + :param bool ssl: Whether the directive includes 'ssl' + :param bool default: Whether the directive includes 'default_server' """ - def __init__(self, tup): - self.tup = tup + def __init__(self, host, port, ssl, default): + self.tup = (host, port) + self.ssl = ssl + self.default = default @classmethod def fromstring(cls, str_addr): """Initialize Addr from string.""" - tup = str_addr.partition(':') - return cls((tup[0], tup[2])) + parts = str_addr.split(' ') + ssl = False + default = False + host = '' + port = '' + + # The first part must be the address + addr = parts.pop(0) + + # Ignore UNIX-domain sockets + if addr.startswith('unix:'): + return None + + tup = addr.partition(':') + if re.match('^\d+$', tup[0]): + # This is a bare port, not a hostname. E.g. listen 80 + host = '' + port = tup[0] + else: + # This is a host-port tuple. E.g. listen 127.0.0.1:* + host = tup[0] + port = tup[2] + + # The rest of the parts are options; we only care about ssl and default + while len(parts) > 0: + nextpart = parts.pop() + if nextpart == 'ssl': + ssl = True + elif nextpart == 'default_server': + default = True + + return cls(host, port, ssl, default) def __str__(self): if self.tup[1]: diff --git a/letsencrypt/client/plugins/nginx/parser.py b/letsencrypt/client/plugins/nginx/parser.py index ad59911ec..acc1a9f36 100644 --- a/letsencrypt/client/plugins/nginx/parser.py +++ b/letsencrypt/client/plugins/nginx/parser.py @@ -113,7 +113,7 @@ class NginxParser(object): :rtype: bool """ - # Look for a server block that contains 'listen [port] ssl' + # Look for a server block that contains 'listen [...] ssl' return False def get_vhosts(self): From efe1f2b2ff39360a3cf07ce1694835f8487755c8 Mon Sep 17 00:00:00 2001 From: yan Date: Wed, 8 Apr 2015 12:52:33 -0700 Subject: [PATCH 14/38] Fill out get_vhosts --- letsencrypt/client/plugins/nginx/obj.py | 3 + letsencrypt/client/plugins/nginx/parser.py | 112 ++++++++++++++------- 2 files changed, 77 insertions(+), 38 deletions(-) diff --git a/letsencrypt/client/plugins/nginx/obj.py b/letsencrypt/client/plugins/nginx/obj.py index 6e20a78b5..835af91b0 100644 --- a/letsencrypt/client/plugins/nginx/obj.py +++ b/letsencrypt/client/plugins/nginx/obj.py @@ -13,6 +13,9 @@ class Addr(object): *:8000 (otherwise). If no port is specified, the default is 80. If no address is specified, listen on all addresses. + .. todo:: Old-style nginx configs define SSL vhosts in a separate block + instead of using 'ssl' in the listen directive + :param str addr: addr part of vhost address, may be hostname, IPv4, IPv6, "", or "*" :param str port: port number or "*" or "" diff --git a/letsencrypt/client/plugins/nginx/parser.py b/letsencrypt/client/plugins/nginx/parser.py index acc1a9f36..361f0c7e2 100644 --- a/letsencrypt/client/plugins/nginx/parser.py +++ b/letsencrypt/client/plugins/nginx/parser.py @@ -84,38 +84,6 @@ class NginxParser(object): return (entry[0] == 'include' and len(entry) == 2 and type(entry[1]) == str) - def _get_names(self, entry): - """Gets server names from nginx parsed entry. - - :param list entry: the parsed entry - :returns: Set of server names - :rtype: set - - """ - return set() - - def _get_addrs(self, entry): - """Gets addresses from nginx parsed entry. - - :param list entry: the parsed entry - :returns: Set of - :class:`~letsencrypt.client.plugins.nginx.obj.Addr` objects - :rtype: set - - """ - return set() - - def _get_ssl(self, entry): - """Gets whether the nginx parsed entry is SSL-enabled. - - :param list entry: the parsed entry - :returns: Whether it's SSL-enabled - :rtype: bool - - """ - # Look for a server block that contains 'listen [...] ssl' - return False - def get_vhosts(self): """Gets list of all 'virtual hosts' found in Nginx configuration. Technically this is a misnomer because Nginx does not have virtual @@ -129,16 +97,64 @@ class NginxParser(object): """ enabled = True # We only look at enabled vhosts for now vhosts = [] + servers = {} # Map of filename to list of parsed server blocks + for filename in self.parsed: tree = self.parsed[filename] - vhost = obj.VirtulHost(filename, - self._get_addrs(tree), - self._get_ssl(tree), - enabled, - self._get_names(tree)) - vhosts.append(vhost) + servers[filename] = [] + + # Find all the server blocks + do_for_subarray(tree, lambda x: x[0] == ['server'], + lambda x: servers[filename].append(x[1])) + + # Find 'include' statements in server blocks and append their trees + for server in servers[filename]: + for directive in server: + if (self._is_include_directive(directive)): + included_files = glob.glob( + self.abs_path(directive[1])) + for f in included_files: + try: + servers[f] = self.parsed[f] + except: + pass + + for filename in servers: + for server in servers[filename]: + # Parse the server block into a VirtualHost object + parsed_server = self._parse_server(server) + vhost = obj.VirtualHost(filename, + parsed_server.addrs, + parsed_server.ssl, + enabled, + parsed_server.names) + vhosts.append(vhost) + return vhosts + def _parse_server(self, server): + """Parses a list of server directives. + + :param list server: list of directives in a server block + :rtype: dict + + """ + parsed_server = {} + parsed_server.addrs = set() + parsed_server.ssl = False + parsed_server.names = set() + + for directive in server: + if directive[0] == 'listen': + addr = obj.Addr.fromstring(directive[1]) + parsed_server.addrs.add(addr) + if not parsed_server.ssl and addr.ssl: + parsed_server.ssl = True + elif directive[0] == 'server_name': + parsed_server.names.update(' '.split(directive[1])) + + return parsed_server + def add_dir_to_ifmodssl(self, aug_conf_path, directive, val): """Adds directive and value to IfMod ssl block. @@ -494,3 +510,23 @@ def strip_dir(path): return path[:index+1] # No directory return "" + + +def do_for_subarray(entry, condition, func): + """Executes a function for a subarray of a nested array if it matches + the given condition. + + :param list entry: The list to iterate over + :param function condition: Returns true iff func should be executed on item + :param function func: The function to call for each matching item + + """ + for item in entry: + if type(item) == list: + if condition(item): + try: + func(item) + except: + logging.warn("Error in do_for_subarray for %s" % item) + else: + do_for_subarray(item, condition, func) From 0ba12c9f464870a7ad4d24b006c9c688aea74d0d Mon Sep 17 00:00:00 2001 From: yan Date: Fri, 17 Apr 2015 22:21:14 -0700 Subject: [PATCH 15/38] Fix typo: _get_vhosts -> get_vhosts --- letsencrypt/client/plugins/nginx/configurator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/letsencrypt/client/plugins/nginx/configurator.py b/letsencrypt/client/plugins/nginx/configurator.py index 4b3239538..1e5e819f8 100644 --- a/letsencrypt/client/plugins/nginx/configurator.py +++ b/letsencrypt/client/plugins/nginx/configurator.py @@ -97,7 +97,7 @@ class NginxConfigurator(object): self.version = self._get_version() # Get all of the available vhosts - self.vhosts = self.parser._get_vhosts() + self.vhosts = self.parser.get_vhosts() temp_install(self.config.nginx_mod_ssl_conf) From 2a869364106dfd4f9bc83f335a103678faff4585 Mon Sep 17 00:00:00 2001 From: yan Date: Fri, 17 Apr 2015 22:22:06 -0700 Subject: [PATCH 16/38] Delete unused methods or replace with placeholders --- .../client/plugins/nginx/configurator.py | 53 +-- .../plugins/nginx/nginx_configurator.py | 208 ------------ letsencrypt/client/plugins/nginx/parser.py | 317 ++---------------- 3 files changed, 36 insertions(+), 542 deletions(-) delete mode 100644 letsencrypt/client/plugins/nginx/nginx_configurator.py diff --git a/letsencrypt/client/plugins/nginx/configurator.py b/letsencrypt/client/plugins/nginx/configurator.py index 1e5e819f8..0e88c4446 100644 --- a/letsencrypt/client/plugins/nginx/configurator.py +++ b/letsencrypt/client/plugins/nginx/configurator.py @@ -162,10 +162,6 @@ class NginxConfigurator(object): if cert_chain: self.save_notes += "\tSSLCertificateChainFile %s\n" % cert_chain - # Make sure vhost is enabled - if not vhost.enabled: - self._enable_site(vhost) - ####################### # Vhost parsing methods ####################### @@ -175,6 +171,9 @@ class NginxConfigurator(object): .. todo:: This should maybe return list if no obvious answer is presented. + .. todo:: The special name "$hostname" corresponds to the machine's + hostname. Currently we just ignore this. + :param str target_name: domain name :returns: ssl vhost associated with name @@ -367,52 +366,6 @@ class NginxConfigurator(object): ###################################### # Nginx server management (IInstaller) ###################################### - def _is_site_enabled(self, avail_fp): - """Checks to see if the given site is enabled. - - .. todo:: fix hardcoded sites-enabled, check os.path.samefile - - :param str avail_fp: Complete file path of available site - - :returns: Success - :rtype: bool - - """ - enabled_dir = os.path.join(self.parser.root, "sites-enabled") - for entry in os.listdir(enabled_dir): - if os.path.realpath(os.path.join(enabled_dir, entry)) == avail_fp: - return True - - return False - - def _enable_site(self, vhost): - """Enables an available site, Nginx restart required. - - .. todo:: This function should number subdomains before the domain vhost - - .. todo:: Make sure link is not broken... - - :param vhost: vhost to enable - :type vhost: :class:`~letsencrypt.client.plugins.nginx.obj.VirtualHost` - - :returns: Success - :rtype: bool - - """ - if self._is_site_enabled(vhost.filep): - return True - - if "/sites-available/" in vhost.filep: - enabled_path = ("%s/sites-enabled/%s" % - (self.parser.root, os.path.basename(vhost.filep))) - self.reverter.register_file_creation(False, enabled_path) - os.symlink(vhost.filep, enabled_path) - vhost.enabled = True - logging.info("Enabling available site: %s", vhost.filep) - self.save_notes += "Enabled site %s\n" % vhost.filep - return True - return False - def restart(self): """Restarts nginx server. diff --git a/letsencrypt/client/plugins/nginx/nginx_configurator.py b/letsencrypt/client/plugins/nginx/nginx_configurator.py deleted file mode 100644 index 86aa7e371..000000000 --- a/letsencrypt/client/plugins/nginx/nginx_configurator.py +++ /dev/null @@ -1,208 +0,0 @@ -import zope.interface - -from letsencrypt.client import augeas_configurator -from letsencrypt.client import CONFIG -from letsencrypt.client import interfaces - - -# This might be helpful... but feel free to use whatever you want -# class VH(object): -# def __init__(self, filename_path, vh_path, vh_addrs, is_ssl, is_enabled): -# self.file = filename_path -# self.path = vh_path -# self.addrs = vh_addrs -# self.names = [] -# self.ssl = is_ssl -# self.enabled = is_enabled - -# def set_names(self, listOfNames): -# self.names = listOfNames - -# def add_name(self, name): -# self.names.append(name) - -class NginxConfigurator(augeas_configurator.AugeasConfigurator): - """Nginx Configurator class.""" - zope.interface.implements(interfaces.IAuthenticator, interfaces.IInstaller) - - def __init__(self, server_root=CONFIG.SERVER_ROOT): - super(NginxConfigurator, self).__init__() - 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 - # vhosts - self.recovery_routine() - # Check for errors in parsing files with Augeas - # TODO - insert nginx lens info here??? - #self.check_parsing_errors("httpd.aug") - - def deploy_cert(self, vhost, cert, key, cert_chain=None): - """Deploy cert in nginx""" - - def choose_virtual_host(self, name): - """Chooses a virtual host based on the given domain name""" - - def get_all_names(self): - """Returns all names found in the nginx configuration""" - return set() - - # Might be helpful... I know nothing about nginx lens - # def get_include_path(self, cur_dir, arg): - # """ - # Converts an Nginx Include directive argument into an Augeas - # searchable path - # Returns path string - # """ - # # 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 Nginx 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 nginx config doesn't work because it - # # would create a race condition between the check and this input - - # # TODO: Fix this - # # 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 Nginx - # elif arg.startswith("conf/"): - # arg = self.server_root + arg[5:] - # # TODO: Test if Nginx 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: - # postfix = "" - # splitArg = arg.split("/") - # for idx, split in enumerate(splitArg): - # # * and ? are the two special fnmatch characters - # if "*" in split or "?" in split: - # # Turn it into a augeas regex - # # TODO: Can this be an augeas glob instead of regex - # splitArg[idx] = ("* [label()=~regexp('%s')]" % - # self.fnmatch_to_re(split) - # # Reassemble the argument - # arg = "/".join(splitArg) - - # # If the include is a directory, just return the directory as a file - # if arg.endswith("/"): - # return "/files" + arg[:len(arg)-1] - # return "/files"+arg - - def enable_redirect(self, ssl_vhost): - """ - Adds Redirect directive to the port 80 equivalent of ssl_vhost - First the function attempts to find the vhost with equivalent - ip addresses that serves on non-ssl ports - The function then adds the directive - """ - return - - def enable_ocsp_stapling(self, ssl_vhost): - return False - - def enable_hsts(self, ssl_vhost): - return False - - def get_all_certs_keys(self): - """ - Retrieve all certs and keys set in VirtualHosts on the Nginx server - returns: list of tuples with form [(cert, key, path)] - """ - return None - - # Probably helpful reference - # def get_file_path(self, vhost_path): - # """ - # Takes in Augeas path and returns the file name - # """ - # # Strip off /files - # avail_fp = vhost_path[6:] - # # This can be optimized... - # while True: - # # Cast both to lowercase to be case insensitive - # find_if = avail_fp.lower().find("/ifmodule") - # if find_if != -1: - # avail_fp = avail_fp[:find_if] - # continue - # find_vh = avail_fp.lower().find("/virtualhost") - # if find_vh != -1: - # avail_fp = avail_fp[:find_vh] - # continue - # break - # return avail_fp - - def enable_site(self, vhost): - """Enables an available site, Nginx restart required""" - return False - - # Might be a usefule reference - # def parse_file(self, file_path): - # """ - # Checks to see if file_path is parsed by Augeas - # If file_path isn't parsed, the file is added and Augeas is reloaded - # """ - # # Test if augeas included file for Httpd.lens - # # Note: This works for augeas globs, ie. *.conf - # incTest = self.aug.match( - # "/augeas/load/Httpd/incl [. ='" + file_path + "']") - # if not incTest: - # # 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() - - # Helpful reference? - # def verify_setup(self): - # """ - # Make sure that files/directories are setup with appropriate - # permissions. Aim for defensive coding... make sure all input files - # have permissions of root - # """ - # le_util.make_or_verify_dir(CONFIG.CONFIG_DIR, 0o755) - # le_util.make_or_verify_dir(CONFIG.WORK_DIR, 0o755) - # le_util.make_or_verify_dir(CONFIG.BACKUP_DIR, 0o755) - - def restart(self, quiet=False): - """Restarts nginx server""" - - # May be of use? - # def __add_httpd_transform(self, incl): - # """ - # This function will correctly add a transform to augeas - # The existing augeas.add_transform in python is broken - # """ - # lastInclude = self.aug.match("/augeas/load/Httpd/incl [last()]") - # self.aug.insert(lastInclude[0], "incl", False) - # self.aug.set("/augeas/load/Httpd/incl[last()]", incl) - - def config_test(self): - """Check Configuration""" - return False - - -def main(): - return - -if __name__ == "__main__": - main() diff --git a/letsencrypt/client/plugins/nginx/parser.py b/letsencrypt/client/plugins/nginx/parser.py index 361f0c7e2..d05bcf13d 100644 --- a/letsencrypt/client/plugins/nginx/parser.py +++ b/letsencrypt/client/plugins/nginx/parser.py @@ -2,7 +2,6 @@ import glob import logging import os -import re import pyparsing from letsencrypt.client import errors @@ -155,224 +154,6 @@ class NginxParser(object): return parsed_server - 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 isinstance(arg, list): - for i, value in enumerate(arg, 1): - self.aug.set( - "%s/directive[last()]/arg[%d]" % (aug_conf_path, i), value) - else: - self.aug.set(aug_conf_path + "/directive[last()]/arg", arg) - - 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 Nginx 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 directive 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 Nginx Include directive into Augeas path. - - Converts an Nginx 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 Nginx 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 nginx 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 - # Nginx 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 Nginx - elif arg.startswith("conf/"): - arg = self.root + arg[4:] - # TODO: Test if Nginx allows ../ or ~/ for Includes - - # Attempts to add a transform to the file if one does not already exist - self._parse_files(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 Nginx's basic fnmatch to regular expression. - - :param str clean_fn_match: Nginx style filename match, similar to globs - - :returns: regex suitable for augeas - :rtype: str - - """ - # Checkout fnmatch.py in venv/local/lib/python2.7/fnmatch.py - regex = "" - for letter in clean_fn_match: - if letter == '.': - regex = regex + r"\." - elif letter == '*': - regex = regex + ".*" - # According to nginx.org ? shouldn't appear - # but in case it is valid... - elif letter == '?': - regex = regex + "." - else: - regex = regex + letter - return regex - def _parse_files(self, filepath): """Parse files from a glob @@ -397,27 +178,6 @@ class NginxParser(object): logging.warn("Could not parse file: %s" % f) return trees - 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 doesn't seem to work for - Travis CI as it loads in libaugeas.so.0.10.0 - - :param str incl: filepath to include for transform - - """ - last_include = self.aug.match("/augeas/load/Httpd/incl [last()]") - if last_include: - # Insert a new node immediately after the last incl - self.aug.insert(last_include[0], "incl", False) - self.aug.set("/augeas/load/Httpd/incl[last()]", incl) - # On first use... must load lens and add file to incl - else: - # Augeas uses base 1 indexing... insert at beginning... - self.aug.set("/augeas/load/Httpd/lens", "Httpd.lns") - self.aug.set("/augeas/load/Httpd/incl", incl) - def _set_locations(self, ssl_options): """Set default location for directives. @@ -450,6 +210,39 @@ class NginxParser(object): raise errors.LetsEncryptNoInstallationError( "Could not find configuration root") + 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 + + """ + pass + + def find_dir(self, directive, arg=None, start=None): + """Finds directive in the configuration. + + Recursively searches through config files to find directives + + .. todo:: Add order to directives returned. Last directive comes last.. + .. todo:: arg should probably be a list + + :param str directive: Directive to look for + + :param arg: Specific value directive must have, None if all should + be considered + :type arg: str or None + + :param str start: Beginning Augeas path to begin looking + :rtype: list + + """ + return [] + def filedump(self, ext='tmp'): """Dumps parsed configurations into files. @@ -468,50 +261,6 @@ class NginxParser(object): logging.error("Could not open file for writing: %s" % filename) -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 "" - - def do_for_subarray(entry, condition, func): """Executes a function for a subarray of a nested array if it matches the given condition. From 3c806b120a27bd12c3b2fa64f90f87b0d1dcbfc7 Mon Sep 17 00:00:00 2001 From: yan Date: Wed, 8 Apr 2015 15:55:30 -0700 Subject: [PATCH 17/38] Update configurator.save and configurator.get_all_names --- .../client/plugins/nginx/configurator.py | 48 +++++++++---------- 1 file changed, 23 insertions(+), 25 deletions(-) diff --git a/letsencrypt/client/plugins/nginx/configurator.py b/letsencrypt/client/plugins/nginx/configurator.py index 0e88c4446..2f95f3c58 100644 --- a/letsencrypt/client/plugins/nginx/configurator.py +++ b/letsencrypt/client/plugins/nginx/configurator.py @@ -35,7 +35,6 @@ class NginxConfigurator(object): :ivar parser: Handles low level parsing :type parser: :class:`~letsencrypt.client.plugins.nginx.parser` - :ivar set save_files: Files that need to be saved :ivar str save_notes: Human-readable config change notes :ivar reverter: saves and reverts checkpoints @@ -67,7 +66,6 @@ class NginxConfigurator(object): self._verify_setup() # Files to save - self.save_files = set() self.save_notes = "" # Add name_server association dict @@ -223,15 +221,23 @@ class NginxConfigurator(object): priv_ip_regex = (r"(^127\.0\.0\.1)|(^10\.)|(^172\.1[6-9]\.)|" r"(^172\.2[0-9]\.)|(^172\.3[0-1]\.)|(^192\.168\.)") private_ips = re.compile(priv_ip_regex) + hostname_regex = r"^(([a-z0-9]|[a-z0-9][a-z0-9\-]*[a-z0-9])\.)*[a-z]+$" + hostnames = re.compile(hostname_regex, re.IGNORECASE) for vhost in self.vhosts: all_names.update(vhost.names) + for addr in vhost.addrs: - # If it isn't a private IP, do a reverse DNS lookup - if not private_ips.match(addr.get_addr()): + host = addr.get_addr() + if hostnames.match(host): + # If it's a hostname, add it to the names. + all_names.add(host) + elif not private_ips.match(host): + # If it isn't a private IP, do a reverse DNS lookup + # TODO: IPv6 support try: - socket.inet_aton(addr.get_addr()) - all_names.add(socket.gethostbyaddr(addr.get_addr())[0]) + socket.inet_aton(host) + all_names.add(socket.gethostbyaddr(host)[0]) except (socket.error, socket.herror, socket.timeout): continue @@ -476,9 +482,6 @@ class NginxConfigurator(object): def save(self, title=None, temporary=False): """Saves all changes to the configuration files. - Working changes are saved in *.conf.le files. This overrides the .conf - file with the .conf.le file contents. - :param str title: The title of the save. If a title is given, the configuration will be saved as a new checkpoint and put in a timestamped directory. @@ -487,23 +490,18 @@ class NginxConfigurator(object): be quickly reversed in the future (ie. challenges) """ - if len(self.save_files) > 0: - # Create Checkpoint - if temporary: - self.reverter.add_to_temp_checkpoint( - self.save_files, self.save_notes) - else: - self.reverter.add_to_checkpoint(self.save_files, - self.save_notes) - # Override the original files with their working copies - for f in self.save_files: - tmpfile = f + '.le' - if (os.path.isfile(tmpfile)): - os.rename(f + '.le', f) - else: - logging.warn("Expected file %s to exist", tmpfile) - self.save_files.remove(f) + save_files = set(self.parser.parsed.keys()) + # Create Checkpoint + if temporary: + self.reverter.add_to_temp_checkpoint( + save_files, self.save_notes) + else: + self.reverter.add_to_checkpoint(save_files, + self.save_notes) + + # Don't override original files for now. + self.parser.filedump('le') if title and not temporary: self.reverter.finalize_checkpoint(title) From 7b72262811a7e76be8ccded8e36e2733137a6705 Mon Sep 17 00:00:00 2001 From: yan Date: Thu, 9 Apr 2015 15:51:58 -0700 Subject: [PATCH 18/38] Fix nginx choose_vhost to use nginx host-choosing rules --- .../client/plugins/nginx/configurator.py | 81 +++++++++++----- letsencrypt/client/plugins/nginx/parser.py | 93 ++++++++++++++++++- 2 files changed, 149 insertions(+), 25 deletions(-) diff --git a/letsencrypt/client/plugins/nginx/configurator.py b/letsencrypt/client/plugins/nginx/configurator.py index 2f95f3c58..65a7ebad5 100644 --- a/letsencrypt/client/plugins/nginx/configurator.py +++ b/letsencrypt/client/plugins/nginx/configurator.py @@ -164,7 +164,8 @@ class NginxConfigurator(object): # Vhost parsing methods ####################### def choose_vhost(self, target_name): - """Chooses a virtual host based on the given domain name. + """Chooses a virtual host based on the given domain name. NOTE: This + makes the vhost SSL-enabled if it isn't already. .. todo:: This should maybe return list if no obvious answer is presented. @@ -178,34 +179,66 @@ class NginxConfigurator(object): :rtype: :class:`~letsencrypt.client.plugins.nginx.obj.VirtualHost` """ - # Allows for domain names to be associated with a virtual host + vhost = None + + # If we already found the vhost for the target, use it if target_name in self.assoc: - return self.assoc[target_name] - # Check for servernames/aliases for ssl hosts - for vhost in self.vhosts: - if vhost.ssl and target_name in vhost.names: - self.assoc[target_name] = vhost - return vhost - # Checking for domain name in vhost address - # This technique is not recommended by Nginx but is technically valid - target_addr = obj.Addr((target_name, "443")) - for vhost in self.vhosts: - if target_addr in vhost.addrs: - self.assoc[target_name] = vhost - return vhost + vhost = self.assoc[target_name] + else: + matches = self._get_ranked_matches(target_name) + if len(matches) == 0: + # No matches at all :'( + break + elif matches[0]['rank'] in range(2, 6): + # Wildcard match - need to find the longest one + rank = matches[0]['rank'] + wildcards = [x for x in matches if x['rank'] == rank] + vhost = max(wildcards, key=lambda x: len(x['name']))['vhost'] + else: + vhost = matches[0]['vhost'] - # Check for non ssl vhosts with servernames/aliases == "name" - for vhost in self.vhosts: - if not vhost.ssl and target_name in vhost.names: + if vhost is not None: + self.assoc[target_name] = vhost + if not vhost.ssl: vhost = self._make_vhost_ssl(vhost) - self.assoc[target_name] = vhost - return vhost - # No matches, search for the default + return vhost + + def _get_ranked_matches(self, target_name): + """ + Returns a ranked list of vhosts that match target_name. + + :param str target_name: The name to match + :returns: list of dicts containing the vhost, the matching name, and + the numerical rank + :rtype: list + + """ + # Nginx chooses a matching server name for a request with precedence: + # 1. exact name match + # 2. longest wildcard name starting with * + # 3. longest wildcard name ending with * + # 4. first matching regex in order of appearance in the file + matches = [] for vhost in self.vhosts: - if "_default_:443" in vhost.addrs: - return vhost - return None + name_type, name = parser.get_best_match(target_name, vhost.names) + if name_type == 'exact': + matches.append({'vhost': vhost, + 'name': name, + 'rank': 0 if vhost.ssl else 1}) + elif name_type == 'wildcard_start': + matches.append({'vhost': vhost, + 'name': name, + 'rank': 2 if vhost.ssl else 3}) + elif name_type == 'wildcard_end': + matches.append({'vhost': vhost, + 'name': name, + 'rank': 4 if vhost.ssl else 5}) + elif name_type == 'regex': + matches.append({'vhost': vhost, + 'name': name, + 'rank': 6 if vhost.ssl else 7}) + return sorted(matches, key=lambda x: x['rank'], reverse=True) def get_all_names(self): """Returns all names found in the Nginx Configuration. diff --git a/letsencrypt/client/plugins/nginx/parser.py b/letsencrypt/client/plugins/nginx/parser.py index d05bcf13d..2633b778c 100644 --- a/letsencrypt/client/plugins/nginx/parser.py +++ b/letsencrypt/client/plugins/nginx/parser.py @@ -3,6 +3,7 @@ import glob import logging import os import pyparsing +import re from letsencrypt.client import errors from letsencrypt.client.plugins.nginx import obj @@ -64,7 +65,7 @@ class NginxParser(object): :param str path: The path :returns: The absolute path - :rtype str + :rtype: str """ if not os.path.isabs(path): @@ -114,6 +115,9 @@ class NginxParser(object): self.abs_path(directive[1])) for f in included_files: try: + # Assign instead of append because servers[f] + # should be empty since server blocks cannot + # contain other server blocks. servers[f] = self.parsed[f] except: pass @@ -279,3 +283,90 @@ def do_for_subarray(entry, condition, func): logging.warn("Error in do_for_subarray for %s" % item) else: do_for_subarray(item, condition, func) + + +def get_best_match(target_name, names): + """Finds the best match for target_name out of names using the Nginx + name-matching rules (exact > longest wildcard starting with * > + longest wildcard ending with * > regex). + + :param str target_name: The name to match + :param list names: The candidate server names + :returns: Tuple of (type of match, the name that matched) + :rtype: tuple + + """ + exact = [] + wildcard_start = [] + wildcard_end = [] + regex = [] + + for name in names: + if _exact_match(target_name, name): + exact.append(name) + elif _wildcard_match(target_name, name, True): + wildcard_start.append(name) + elif _wildcard_match(target_name, name, False): + wildcard_end.append(name) + elif _regex_match(target_name, name): + regex.append(name) + + if len(exact) > 0: + # There can be more than one exact match; e.g. eff.org, .eff.org + match = min(exact, key=lambda x: len(x)) + return ('exact', match) + if len(wildcard_start) > 0: + # Return the longest wildcard + match = max(wildcard_start, key=lambda x: len(x)) + return ('wildcard_start', match) + if len(wildcard_end) > 0: + # Return the longest wildcard + match = max(wildcard_end, key=lambda x: len(x)) + return ('wildcard_end', match) + if len(regex) > 0: + # Just return the first one for now + match = regex[0] + return ('regex', match) + + return (None, None) + + +def _exact_match(target_name, name): + return (target_name == name or target_name == '.' + name) + + +def _wildcard_match(target_name, name, start): + parts = target_name.split('.') + match_parts = name.split('.') + + # If the domain ends in a wildcard, do the match procedure in reverse + if not start: + parts.reverse() + match_parts.reverse() + + # The first part must be a wildcard + if match_parts.pop(0) != '*': + return False + + target_name = '.'.join(parts) + name = '.'.join(match_parts) + + # Ex: www.eff.org matches *.eff.org, eff.org does not match *.eff.org + return target_name.endswith('.' + name) + + +def _regex_match(target_name, name): + # Must start with a tilde + if name[0] != '~': + return False + + # After tilde is a perl-compatible regex + try: + regex = re.compile(name[1:]) + if regex.match(target_name): + return True + else: + return False + except: + # perl-compatible regexes are sometimes not recognized by python + return False From 2a9c707dbdc6270e5e7724bab7f244bcf84cffb8 Mon Sep 17 00:00:00 2001 From: yan Date: Fri, 10 Apr 2015 11:45:05 -0700 Subject: [PATCH 19/38] Update method to make server SSL ready --- .../client/plugins/nginx/configurator.py | 100 ++++-------------- letsencrypt/client/plugins/nginx/obj.py | 8 +- .../client/plugins/nginx/options-ssl.conf | 31 ++---- letsencrypt/client/plugins/nginx/parser.py | 26 ++++- 4 files changed, 53 insertions(+), 112 deletions(-) diff --git a/letsencrypt/client/plugins/nginx/configurator.py b/letsencrypt/client/plugins/nginx/configurator.py index 65a7ebad5..ba4fc77e2 100644 --- a/letsencrypt/client/plugins/nginx/configurator.py +++ b/letsencrypt/client/plugins/nginx/configurator.py @@ -19,7 +19,6 @@ from letsencrypt.client import le_util from letsencrypt.client import reverter from letsencrypt.client.plugins.nginx import dvsni -from letsencrypt.client.plugins.nginx import obj from letsencrypt.client.plugins.nginx import parser @@ -165,7 +164,8 @@ class NginxConfigurator(object): ####################### def choose_vhost(self, target_name): """Chooses a virtual host based on the given domain name. NOTE: This - makes the vhost SSL-enabled if it isn't already. + makes the vhost SSL-enabled if it isn't already. Follows Nginx's server + block selection rules but prefers blocks that are already SSL. .. todo:: This should maybe return list if no obvious answer is presented. @@ -200,7 +200,7 @@ class NginxConfigurator(object): if vhost is not None: self.assoc[target_name] = vhost if not vhost.ssl: - vhost = self._make_vhost_ssl(vhost) + self._make_server_ssl(vhost.filep, vhost.names) return vhost @@ -276,83 +276,23 @@ class NginxConfigurator(object): return all_names - def _make_vhost_ssl(self, nonssl_vhost): # pylint: disable=too-many-locals - """Makes an ssl_vhost version of a nonssl_vhost. + def _make_server_ssl(self, filename, names): + """Makes a server SSL based on server_name and filename by adding + a 'listen 443 ssl' directive to the server block. - Duplicates vhost and adds default ssl options - New vhost will reside as (nonssl_vhost.path) + ``IConfig.le_vhost_ext`` + .. todo:: Maybe this should create a new block instead of modifying + the existing one? - .. note:: This function saves the configuration - - :param nonssl_vhost: Valid VH that doesn't have SSLEngine on - :type nonssl_vhost: - :class:`~letsencrypt.client.plugins.nginx.obj.VirtualHost` - - :returns: SSL vhost - :rtype: :class:`~letsencrypt.client.plugins.nginx.obj.VirtualHost` + :param str filename: The absolute filename of the config file. + :param set names: The server names of the block to add SSL in """ - avail_fp = nonssl_vhost.filep - # Get filepath of new ssl_vhost - if avail_fp.endswith(".conf"): - ssl_fp = avail_fp[:-(len(".conf"))] + self.config.le_vhost_ext - else: - ssl_fp = avail_fp + self.config.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("\n") - for line in orig_file: - new_file.write(line) - new_file.write("\n") - except IOError: - logging.fatal("Error writing/reading to file in _make_vhost_ssl") - sys.exit(49) - - 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 = obj.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: - logging.error("Error: should only be one vhost in %s", avail_fp) - sys.exit(1) - - 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) - 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) - - return ssl_vhost + self.parser.add_server_directives( + filename, names, + [['listen', '443 ssl'], + ['ssl_certificate', '/etc/ssl/certs/ssl-cert-snakeoil.pem'], + ['ssl_key', '/etc/ssl/private/ssl-cert-snakeoil.key'], + ['include', self.parser.loc["ssl_options"]]]) def get_all_certs_keys(self): """Find all existing keys, certs from configuration. @@ -490,12 +430,12 @@ class NginxConfigurator(object): nginx_version = tuple([int(i) for i in version_matches[0].split(".")]) - # nginx <= 0.7.14 has an incompatible SSL configuration format + # nginx < 0.8.21 doesn't use default_server if (nginx_version[0] == 0 and - (nginx_version[1] < 7 or - (nginx_version[1] == 7 and nginx_version[2] < 15))): + (nginx_version[1] < 8 or + (nginx_version[1] == 8 and nginx_version[2] < 21))): raise errors.LetsEncryptConfiguratorError( - "Nginx version not supported") + "Nginx version must be 0.8.21+") return nginx_version diff --git a/letsencrypt/client/plugins/nginx/obj.py b/letsencrypt/client/plugins/nginx/obj.py index 835af91b0..6ac48fd7f 100644 --- a/letsencrypt/client/plugins/nginx/obj.py +++ b/letsencrypt/client/plugins/nginx/obj.py @@ -103,19 +103,15 @@ class VirtualHost(object): # pylint: disable=too-few-public-methods """ - def __init__(self, filep, addrs, ssl, enabled, names=None): + def __init__(self, filep, addrs, ssl, enabled, names): # pylint: disable=too-many-arguments """Initialize a VH.""" self.filep = filep self.addrs = addrs - self.names = set() if names is None else set(names) + self.names = names self.ssl = ssl self.enabled = enabled - def add_name(self, name): - """Add name to vhost.""" - self.names.add(name) - def __str__(self): addr_str = ", ".join(str(addr) for addr in self.addrs) return ("file: %s\n" diff --git a/letsencrypt/client/plugins/nginx/options-ssl.conf b/letsencrypt/client/plugins/nginx/options-ssl.conf index 8380542c0..f0081c1fc 100644 --- a/letsencrypt/client/plugins/nginx/options-ssl.conf +++ b/letsencrypt/client/plugins/nginx/options-ssl.conf @@ -1,27 +1,8 @@ -ssl_session_cache shared:SSL:1m; # 1MB is ~4000 sessions, if it fills old sessions are dropped -ssl_session_timeout 1440m; # Reuse sessions for 24hrs +ssl_session_cache shared:SSL:1m; +ssl_session_timeout 1440m; -# Redirect all traffic to SSL -server { - listen 80 default; - server_name www.example.com example.com; - access_log off; - error_log off; - return 301 https://example.com$request_uri; -} +ssl_protocols TLSv1 TLSv1.1 TLSv1.2; +ssl_prefer_server_ciphers on; -server { - listen 443 ssl default_server; - server_name example.com; - - ssl_certificate /path/to/bundle.crt; - ssl_certificate_key /path/to/private.key; - - ssl_protocols TLSv1 TLSv1.1 TLSv1.2; - ssl_prefer_server_ciphers on; - - # Using list of ciphers from "Bulletproof SSL and TLS" - ssl_ciphers "ECDHE-ECDSA-AES128-GCM-SHA256 ECDHE-ECDSA-AES256-GCM-SHA384 ECDHE-ECDSA-AES128-SHA ECDHE-ECDSA-AES256-SHA ECDHE-ECDSA-AES128-SHA256 ECDHE-ECDSA-AES256-SHA384 ECDHE-RSA-AES128-GCM-SHA256 ECDHE-RSA-AES256-GCM-SHA384 ECDHE-RSA-AES128-SHA ECDHE-RSA-AES128-SHA256 ECDHE-RSA-AES256-SHA384 DHE-RSA-AES128-GCM-SHA256 DHE-RSA-AES256-GCM-SHA384 DHE-RSA-AES128-SHA DHE-RSA-AES256-SHA DHE-RSA-AES128-SHA256 DHE-RSA-AES256-SHA256 EDH-RSA-DES-CBC3-SHA"; - - # Normal stuff below here -} +# Using list of ciphers from "Bulletproof SSL and TLS" +ssl_ciphers "ECDHE-ECDSA-AES128-GCM-SHA256 ECDHE-ECDSA-AES256-GCM-SHA384 ECDHE-ECDSA-AES128-SHA ECDHE-ECDSA-AES256-SHA ECDHE-ECDSA-AES128-SHA256 ECDHE-ECDSA-AES256-SHA384 ECDHE-RSA-AES128-GCM-SHA256 ECDHE-RSA-AES256-GCM-SHA384 ECDHE-RSA-AES128-SHA ECDHE-RSA-AES128-SHA256 ECDHE-RSA-AES256-SHA384 DHE-RSA-AES128-GCM-SHA256 DHE-RSA-AES256-GCM-SHA384 DHE-RSA-AES128-SHA DHE-RSA-AES256-SHA DHE-RSA-AES128-SHA256 DHE-RSA-AES256-SHA256 EDH-RSA-DES-CBC3-SHA"; diff --git a/letsencrypt/client/plugins/nginx/parser.py b/letsencrypt/client/plugins/nginx/parser.py index 2633b778c..4ff29962c 100644 --- a/letsencrypt/client/plugins/nginx/parser.py +++ b/letsencrypt/client/plugins/nginx/parser.py @@ -264,6 +264,30 @@ class NginxParser(object): except IOError: logging.error("Could not open file for writing: %s" % filename) + def add_server_directives(self, filename, names, directives): + """Adds directives to a server block whose server_name set is 'names'. + + :param str filename: The absolute filename of the config file + :param str names: The server_name to match + :param list directives: The directives to add + + """ + if len(names) == 0: + # Nothing to identify blocks with + return False + + def has_server_names(entry): + # Checks if a server block has the given names + # TODO: Make this work if some of the names are in included files + server_names = set() + for item in entry: + if item[0] == 'server_name': + server_names.update((' ').split(item[1])) + return server_names == names + + do_for_subarray(self.parsed[filename], lambda x: has_server_names(x), + lambda x: x.extend(directives)) + def do_for_subarray(entry, condition, func): """Executes a function for a subarray of a nested array if it matches @@ -291,7 +315,7 @@ def get_best_match(target_name, names): longest wildcard ending with * > regex). :param str target_name: The name to match - :param list names: The candidate server names + :param set names: The candidate server names :returns: Tuple of (type of match, the name that matched) :rtype: tuple From e5a027ce307702adb700a6478398f00f3b0dcb21 Mon Sep 17 00:00:00 2001 From: yan Date: Fri, 10 Apr 2015 15:21:25 -0700 Subject: [PATCH 20/38] Make nginx deploy_cert --- .../client/plugins/nginx/configurator.py | 57 +++------ letsencrypt/client/plugins/nginx/parser.py | 120 +++++++++++------- 2 files changed, 87 insertions(+), 90 deletions(-) diff --git a/letsencrypt/client/plugins/nginx/configurator.py b/letsencrypt/client/plugins/nginx/configurator.py index ba4fc77e2..2d73196ef 100644 --- a/letsencrypt/client/plugins/nginx/configurator.py +++ b/letsencrypt/client/plugins/nginx/configurator.py @@ -100,18 +100,11 @@ class NginxConfigurator(object): # Entry point in main.py for installing cert def deploy_cert(self, domain, cert, key, cert_chain=None): - """Deploys certificate to specified virtual host. + """Deploys certificate to specified virtual host. Aborts if the + vhost is missing ssl_certificate or ssl_certificate_key. - 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. - - .. 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 + Nginx doesn't have a cert chain directive, so the last parameter is + always ignored. It expects the cert file to have the concatenated chain. :param str domain: domain to deploy certificate :param str cert: certificate filename @@ -120,44 +113,26 @@ class NginxConfigurator(object): """ vhost = self.choose_vhost(domain) - path = {} + directives = [['ssl_certificate', cert], ['ssl_certificate_key', key]] - path["cert_file"] = 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 cert_chain is not None: - 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" + try: + self.parser.add_server_directives(vhost.filep, vhost.names, + directives, True) + logging.info("Deployed Certificate to VirtualHost %s for %s", + vhost.filep, vhost.names) + except errors.LetsEncryptMisconfigurationError: logging.warn( - "Cannot find a cert or key directive in %s", vhost.path) + "Cannot find a cert or key directive in %s for %s", + vhost.filep, vhost.names) logging.warn("VirtualHost was not modified") # Presumably break here so that the virtualhost is not modified return False - logging.info("Deploying Certificate to VirtualHost %s", vhost.filep) - - self.aug.set(path["cert_file"][0], cert) - self.aug.set(path["cert_key"][0], key) - if cert_chain is not None: - if len(path["cert_chain"]) == 0: - self.parser.add_dir( - vhost.path, "SSLCertificateChainFile", cert_chain) - else: - self.aug.set(path["cert_chain"][0], cert_chain) - 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 - self.save_notes += "\tSSLCertificateKeyFile %s\n" % key - if cert_chain: - self.save_notes += "\tSSLCertificateChainFile %s\n" % cert_chain + self.save_notes += "\tssl_certificate %s\n" % cert + self.save_notes += "\tssl_certificate_key %s\n" % key ####################### # Vhost parsing methods @@ -291,7 +266,7 @@ class NginxConfigurator(object): filename, names, [['listen', '443 ssl'], ['ssl_certificate', '/etc/ssl/certs/ssl-cert-snakeoil.pem'], - ['ssl_key', '/etc/ssl/private/ssl-cert-snakeoil.key'], + ['ssl_certificate_key', '/etc/ssl/private/ssl-cert-snakeoil.key'], ['include', self.parser.loc["ssl_options"]]]) def get_all_certs_keys(self): diff --git a/letsencrypt/client/plugins/nginx/parser.py b/letsencrypt/client/plugins/nginx/parser.py index 4ff29962c..39cdc09d6 100644 --- a/letsencrypt/client/plugins/nginx/parser.py +++ b/letsencrypt/client/plugins/nginx/parser.py @@ -81,7 +81,8 @@ class NginxParser(object): :rtype: bool """ - return (entry[0] == 'include' and len(entry) == 2 and + return (type(entry) == list and + entry[0] == 'include' and len(entry) == 2 and type(entry[1]) == str) def get_vhosts(self): @@ -214,39 +215,6 @@ class NginxParser(object): raise errors.LetsEncryptNoInstallationError( "Could not find configuration root") - 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 - - """ - pass - - def find_dir(self, directive, arg=None, start=None): - """Finds directive in the configuration. - - Recursively searches through config files to find directives - - .. todo:: Add order to directives returned. Last directive comes last.. - .. todo:: arg should probably be a list - - :param str directive: Directive to look for - - :param arg: Specific value directive must have, None if all should - be considered - :type arg: str or None - - :param str start: Beginning Augeas path to begin looking - :rtype: list - - """ - return [] - def filedump(self, ext='tmp'): """Dumps parsed configurations into files. @@ -264,29 +232,83 @@ class NginxParser(object): except IOError: logging.error("Could not open file for writing: %s" % filename) - def add_server_directives(self, filename, names, directives): - """Adds directives to a server block whose server_name set is 'names'. + def _has_server_names(self, entry, names): + """Checks if a server block has the given set of server_names. This + is the primary way of identifying server blocks in the configurator. + Returns false if 'entry' doesn't look like a server block at all. - :param str filename: The absolute filename of the config file - :param str names: The server_name to match - :param list directives: The directives to add + ..todo :: Doesn't match server blocks whose server_name directives are + split across multiple conf files. + + :param list entry: The block to search + :param set names: The names to match + :rtype: bool """ if len(names) == 0: # Nothing to identify blocks with return False - def has_server_names(entry): - # Checks if a server block has the given names - # TODO: Make this work if some of the names are in included files - server_names = set() - for item in entry: - if item[0] == 'server_name': - server_names.update((' ').split(item[1])) - return server_names == names + if type(entry) != list: + # Can't be a server block + return False - do_for_subarray(self.parsed[filename], lambda x: has_server_names(x), - lambda x: x.extend(directives)) + server_names = set() + for item in entry: + if type(item) != list: + # Can't be a server block + return False + + if item[0] == 'server_name': + server_names.update((' ').split(item[1])) + + return server_names == names + + def _replace_directives(self, block, directives): + """Replaces directives in a block. If the directive doesn't exist in + the entry already, raises a misconfiguration error. + + ..todo :: Find directives that are in included files. + + :param list block: The block to replace in + :param list directives: The new directives. + """ + for directive in directives: + changed = False + if len(directive) == 0: + continue + for line in block: + if len(line) > 0 and line[0] == directive[0]: + line = directive + changed = True + if not changed: + raise errors.LetsEncryptMisconfigurationError( + 'LetsEncrypt expected directive for %s in the Nginx config ' + 'but did not find it.' % directive[0]) + + def add_server_directives(self, filename, names, directives, + replace=False): + """Add or replace directives in server blocks whose server_name set + is 'names'. If replace is True, this raises a misconfiguration error + if the directive does not already exist. + + ..todo :: Doesn't match server blocks whose server_name directives are + split across multiple conf files. + + :param str filename: The absolute filename of the config file + :param str names: The server_name to match + :param list directives: The directives to add + :param bool replace: Whether to only replace existing directives + + """ + if replace: + do_for_subarray(self.parsed[filename], + lambda x: self._has_server_names(x, names), + lambda x: self._replace_directives(x, directives)) + else: + do_for_subarray(self.parsed[filename], + lambda x: self._has_server_names(x, names), + lambda x: x.extend(directives)) def do_for_subarray(entry, condition, func): From d9c8c13f9ac28b8507403f0f2479d4829954a3d9 Mon Sep 17 00:00:00 2001 From: yan Date: Fri, 10 Apr 2015 18:17:17 -0700 Subject: [PATCH 21/38] Fix and add test for get_vhosts --- .../client/plugins/nginx/configurator.py | 2 +- letsencrypt/client/plugins/nginx/obj.py | 6 +- letsencrypt/client/plugins/nginx/parser.py | 61 +++++++++++-------- .../client/plugins/nginx/tests/parser_test.py | 49 +++++++++++++-- .../plugins/nginx/tests/testdata/foo.conf | 4 +- .../tests/testdata/sites-enabled/default | 4 +- .../tests/testdata/sites-enabled/example.com | 3 +- 7 files changed, 91 insertions(+), 38 deletions(-) diff --git a/letsencrypt/client/plugins/nginx/configurator.py b/letsencrypt/client/plugins/nginx/configurator.py index 2d73196ef..48d44c5f8 100644 --- a/letsencrypt/client/plugins/nginx/configurator.py +++ b/letsencrypt/client/plugins/nginx/configurator.py @@ -163,7 +163,7 @@ class NginxConfigurator(object): matches = self._get_ranked_matches(target_name) if len(matches) == 0: # No matches at all :'( - break + pass elif matches[0]['rank'] in range(2, 6): # Wildcard match - need to find the longest one rank = matches[0]['rank'] diff --git a/letsencrypt/client/plugins/nginx/obj.py b/letsencrypt/client/plugins/nginx/obj.py index 6ac48fd7f..3eaee5a41 100644 --- a/letsencrypt/client/plugins/nginx/obj.py +++ b/letsencrypt/client/plugins/nginx/obj.py @@ -71,7 +71,9 @@ class Addr(object): def __eq__(self, other): if isinstance(other, self.__class__): - return self.tup == other.tup + return (self.tup == other.tup and + self.ssl == other.ssl and + self.default == other.default) return False def __hash__(self): @@ -124,7 +126,7 @@ class VirtualHost(object): # pylint: disable=too-few-public-methods def __eq__(self, other): if isinstance(other, self.__class__): return (self.filep == other.filep and - self.addrs == other.addrs and + list(self.addrs) == list(other.addrs) and self.names == other.names and self.ssl == other.ssl and self.enabled == other.enabled) diff --git a/letsencrypt/client/plugins/nginx/parser.py b/letsencrypt/client/plugins/nginx/parser.py index 39cdc09d6..b6a75344e 100644 --- a/letsencrypt/client/plugins/nginx/parser.py +++ b/letsencrypt/client/plugins/nginx/parser.py @@ -105,8 +105,8 @@ class NginxParser(object): servers[filename] = [] # Find all the server blocks - do_for_subarray(tree, lambda x: x[0] == ['server'], - lambda x: servers[filename].append(x[1])) + _do_for_subarray(tree, lambda x: x[0] == ['server'], + lambda x: servers[filename].append(x[1])) # Find 'include' statements in server blocks and append their trees for server in servers[filename]: @@ -116,10 +116,7 @@ class NginxParser(object): self.abs_path(directive[1])) for f in included_files: try: - # Assign instead of append because servers[f] - # should be empty since server blocks cannot - # contain other server blocks. - servers[f] = self.parsed[f] + server.extend(self.parsed[f]) except: pass @@ -128,10 +125,10 @@ class NginxParser(object): # Parse the server block into a VirtualHost object parsed_server = self._parse_server(server) vhost = obj.VirtualHost(filename, - parsed_server.addrs, - parsed_server.ssl, + parsed_server['addrs'], + parsed_server['ssl'], enabled, - parsed_server.names) + parsed_server['names']) vhosts.append(vhost) return vhosts @@ -144,21 +141,33 @@ class NginxParser(object): """ parsed_server = {} - parsed_server.addrs = set() - parsed_server.ssl = False - parsed_server.names = set() + parsed_server['addrs'] = set() + parsed_server['ssl'] = False + parsed_server['names'] = set() for directive in server: if directive[0] == 'listen': addr = obj.Addr.fromstring(directive[1]) - parsed_server.addrs.add(addr) - if not parsed_server.ssl and addr.ssl: - parsed_server.ssl = True + parsed_server['addrs'].add(addr) + if not parsed_server['ssl'] and addr.ssl: + parsed_server['ssl'] = True elif directive[0] == 'server_name': - parsed_server.names.update(' '.split(directive[1])) + parsed_server['names'].update( + self._get_servernames(directive[1])) return parsed_server + def _get_servernames(self, names): + """Turns a server_name string into a list of server names + + :param str names: server names + :rtype: list + + """ + whitespace_re = re.compile(r'\s+') + names = re.sub(whitespace_re, ' ', names) + return names.split(' ') + def _parse_files(self, filepath): """Parse files from a glob @@ -260,7 +269,7 @@ class NginxParser(object): return False if item[0] == 'server_name': - server_names.update((' ').split(item[1])) + server_names.update(self._get_servernames(item[1])) return server_names == names @@ -302,16 +311,16 @@ class NginxParser(object): """ if replace: - do_for_subarray(self.parsed[filename], - lambda x: self._has_server_names(x, names), - lambda x: self._replace_directives(x, directives)) + _do_for_subarray(self.parsed[filename], + lambda x: self._has_server_names(x, names), + lambda x: self._replace_directives(x, directives)) else: - do_for_subarray(self.parsed[filename], - lambda x: self._has_server_names(x, names), - lambda x: x.extend(directives)) + _do_for_subarray(self.parsed[filename], + lambda x: self._has_server_names(x, names), + lambda x: x.extend(directives)) -def do_for_subarray(entry, condition, func): +def _do_for_subarray(entry, condition, func): """Executes a function for a subarray of a nested array if it matches the given condition. @@ -326,9 +335,9 @@ def do_for_subarray(entry, condition, func): try: func(item) except: - logging.warn("Error in do_for_subarray for %s" % item) + logging.warn("Error in _do_for_subarray for %s" % item) else: - do_for_subarray(item, condition, func) + _do_for_subarray(item, condition, func) def get_best_match(target_name, names): diff --git a/letsencrypt/client/plugins/nginx/tests/parser_test.py b/letsencrypt/client/plugins/nginx/tests/parser_test.py index 4502c5859..28fa7057e 100644 --- a/letsencrypt/client/plugins/nginx/tests/parser_test.py +++ b/letsencrypt/client/plugins/nginx/tests/parser_test.py @@ -5,12 +5,11 @@ import shutil import sys import unittest -import mock import zope.component -from letsencrypt.client import errors from letsencrypt.client.display import util as display_util +from letsencrypt.client.plugins.nginx.obj import Addr, VirtualHost from letsencrypt.client.plugins.nginx.parser import NginxParser from letsencrypt.client.plugins.nginx.tests import util @@ -56,7 +55,8 @@ class NginxParserTest(util.NginxTest): self.assertEqual([['server_name', 'somename alias another.alias']], parser.parsed[parser.abs_path('server.conf')]) self.assertEqual([[['server'], [['listen', '9000'], - ['server_name', 'example.com']]]], + ['server_name', '.example.com'], + ['server_name', 'example.*']]]], parser.parsed[parser.abs_path( 'sites-enabled/example.com')]) @@ -76,9 +76,50 @@ class NginxParserTest(util.NginxTest): self.assertEqual(2, len( glob.glob(parser.abs_path('sites-enabled/*.test')))) self.assertEqual([[['server'], [['listen', '9000'], - ['server_name', 'example.com']]]], + ['server_name', '.example.com'], + ['server_name', 'example.*']]]], parsed[0]) + def test_get_vhosts(self): + parser = NginxParser(self.config_path, self.ssl_options) + vhosts = parser.get_vhosts() + + vhost1 = VirtualHost(parser.abs_path('nginx.conf'), + [Addr('', '8080', False, False)], + False, True, set(['localhost'])) + vhost2 = VirtualHost(parser.abs_path('nginx.conf'), + [Addr('somename', '8080', False, False), + Addr('', '8000', False, False)], + False, True, set(['somename', + 'another.alias', 'alias'])) + vhost3 = VirtualHost(parser.abs_path('sites-enabled/example.com'), + [Addr('', '9000', False, False)], + False, True, set(['.example.com', 'example.*'])) + vhost4 = VirtualHost(parser.abs_path('sites-enabled/default'), + [Addr('myhost', '', False, True)], + False, True, set(['www.example.org'])) + vhost5 = VirtualHost(parser.abs_path('foo.conf'), + [Addr('*', '80', True, True)], + True, True, set(['*.www.foo.com'])) + + self.assertEqual(5, len(vhosts)) + example_com = filter(lambda x: 'example.com' in x.filep, vhosts)[0] + self.assertEqual(vhost3, example_com) + default = filter(lambda x: 'default' in x.filep, vhosts)[0] + self.assertEqual(vhost4, default) + foo = filter(lambda x: 'foo.conf' in x.filep, vhosts)[0] + self.assertEqual(vhost5, foo) + localhost = filter(lambda x: 'localhost' in x.names, vhosts)[0] + self.assertEquals(vhost1, localhost) + somename = filter(lambda x: 'somename' in x.names, vhosts)[0] + self.assertEquals(vhost2, somename) + + def test_add_server_directives(self): + pass + + def test_get_best_match(self): + pass + # def test_find_dir(self): # from letsencrypt.client.plugins.nginx.parser import case_i # test = self.parser.find_dir(case_i("Listen"), "443") diff --git a/letsencrypt/client/plugins/nginx/tests/testdata/foo.conf b/letsencrypt/client/plugins/nginx/tests/testdata/foo.conf index f68ce9ceb..56ae5b33c 100644 --- a/letsencrypt/client/plugins/nginx/tests/testdata/foo.conf +++ b/letsencrypt/client/plugins/nginx/tests/testdata/foo.conf @@ -2,8 +2,8 @@ user www-data; server { - listen 80; - server_name foo.com; + listen *:80 default_server ssl; + server_name *.www.foo.com; root /home/ubuntu/sites/foo/; location /status { diff --git a/letsencrypt/client/plugins/nginx/tests/testdata/sites-enabled/default b/letsencrypt/client/plugins/nginx/tests/testdata/sites-enabled/default index 29a311cee..26f37020c 100644 --- a/letsencrypt/client/plugins/nginx/tests/testdata/sites-enabled/default +++ b/letsencrypt/client/plugins/nginx/tests/testdata/sites-enabled/default @@ -1,6 +1,6 @@ server { - listen 1234; - server_name example.org; + listen myhost default_server; + server_name www.example.org; location / { root html; diff --git a/letsencrypt/client/plugins/nginx/tests/testdata/sites-enabled/example.com b/letsencrypt/client/plugins/nginx/tests/testdata/sites-enabled/example.com index d61f8a698..bea8d7a3b 100644 --- a/letsencrypt/client/plugins/nginx/tests/testdata/sites-enabled/example.com +++ b/letsencrypt/client/plugins/nginx/tests/testdata/sites-enabled/example.com @@ -1,4 +1,5 @@ server { listen 9000; - server_name example.com; + server_name .example.com; + server_name example.*; } From fe1ba9dad68909125326c05b3b2b3f8deef572e6 Mon Sep 17 00:00:00 2001 From: yan Date: Mon, 13 Apr 2015 22:57:06 -0700 Subject: [PATCH 22/38] Add test for nginx name matching --- letsencrypt/client/plugins/nginx/parser.py | 37 +++--- .../client/plugins/nginx/tests/parser_test.py | 125 +++++++++--------- 2 files changed, 86 insertions(+), 76 deletions(-) diff --git a/letsencrypt/client/plugins/nginx/parser.py b/letsencrypt/client/plugins/nginx/parser.py index b6a75344e..52b02e9e1 100644 --- a/letsencrypt/client/plugins/nginx/parser.py +++ b/letsencrypt/client/plugins/nginx/parser.py @@ -286,9 +286,9 @@ class NginxParser(object): changed = False if len(directive) == 0: continue - for line in block: + for index, line in enumerate(block): if len(line) > 0 and line[0] == directive[0]: - line = directive + block[index] = directive changed = True if not changed: raise errors.LetsEncryptMisconfigurationError( @@ -305,7 +305,7 @@ class NginxParser(object): split across multiple conf files. :param str filename: The absolute filename of the config file - :param str names: The server_name to match + :param set names: The server_name to match :param list directives: The directives to add :param bool replace: Whether to only replace existing directives @@ -329,14 +329,11 @@ def _do_for_subarray(entry, condition, func): :param function func: The function to call for each matching item """ - for item in entry: - if type(item) == list: - if condition(item): - try: - func(item) - except: - logging.warn("Error in _do_for_subarray for %s" % item) - else: + if type(entry) == list: + if condition(entry): + func(entry) + else: + for item in entry: _do_for_subarray(item, condition, func) @@ -387,10 +384,14 @@ def get_best_match(target_name, names): def _exact_match(target_name, name): - return (target_name == name or target_name == '.' + name) + return (target_name == name or '.' + target_name == name) def _wildcard_match(target_name, name, start): + # Degenerate case + if name == '*': + return True + parts = target_name.split('.') match_parts = name.split('.') @@ -399,8 +400,12 @@ def _wildcard_match(target_name, name, start): parts.reverse() match_parts.reverse() - # The first part must be a wildcard - if match_parts.pop(0) != '*': + if len(match_parts) == 0: + return False + + # The first part must be a wildcard or blank, e.g. '.eff.org' + first = match_parts.pop(0) + if first != '*' and first != '': return False target_name = '.'.join(parts) @@ -412,13 +417,13 @@ def _wildcard_match(target_name, name, start): def _regex_match(target_name, name): # Must start with a tilde - if name[0] != '~': + if len(name) < 2 or name[0] != '~': return False # After tilde is a perl-compatible regex try: regex = re.compile(name[1:]) - if regex.match(target_name): + if re.match(regex, target_name): return True else: return False diff --git a/letsencrypt/client/plugins/nginx/tests/parser_test.py b/letsencrypt/client/plugins/nginx/tests/parser_test.py index 28fa7057e..55c7f5405 100644 --- a/letsencrypt/client/plugins/nginx/tests/parser_test.py +++ b/letsencrypt/client/plugins/nginx/tests/parser_test.py @@ -1,6 +1,7 @@ """Tests for letsencrypt.client.plugins.nginx.parser.""" import glob import os +import re import shutil import sys import unittest @@ -8,9 +9,11 @@ import unittest import zope.component from letsencrypt.client.display import util as display_util +from letsencrypt.client.errors import LetsEncryptMisconfigurationError +from letsencrypt.client.plugins.nginx.nginxparser import dumps from letsencrypt.client.plugins.nginx.obj import Addr, VirtualHost -from letsencrypt.client.plugins.nginx.parser import NginxParser +from letsencrypt.client.plugins.nginx.parser import NginxParser, get_best_match from letsencrypt.client.plugins.nginx.tests import util @@ -115,68 +118,70 @@ class NginxParserTest(util.NginxTest): self.assertEquals(vhost2, somename) def test_add_server_directives(self): - pass + parser = NginxParser(self.config_path, self.ssl_options) + parser.add_server_directives(parser.abs_path('nginx.conf'), + set(['localhost']), + [['foo', 'bar'], ['ssl_certificate', + '/etc/ssl/cert.pem']]) + r = re.compile('foo bar;\n\s+ssl_certificate /etc/ssl/cert.pem') + self.assertEqual(1, len(re.findall(r, dumps(parser.parsed[ + parser.abs_path('nginx.conf')])))) + parser.add_server_directives(parser.abs_path('server.conf'), + set(['alias', 'another.alias', + 'somename']), + [['foo', 'bar'], ['ssl_certificate', + '/etc/ssl/cert2.pem']]) + self.assertEqual(parser.parsed[parser.abs_path('server.conf')], + [['server_name', 'somename alias another.alias'], + ['foo', 'bar'], + ['ssl_certificate', '/etc/ssl/cert2.pem']]) + + def test_replace_server_directives(self): + parser = NginxParser(self.config_path, self.ssl_options) + target = set(['.example.com', 'example.*']) + filep = parser.abs_path('sites-enabled/example.com') + parser.add_server_directives( + filep, target, [['server_name', 'foo bar']], True) + self.assertEqual( + parser.parsed[filep], + [[['server'], [['listen', '9000'], ['server_name', 'foo bar'], + ['server_name', 'foo bar']]]]) + self.assertRaises(LetsEncryptMisconfigurationError, + parser.add_server_directives, + filep, set(['foo', 'bar']), + [['ssl_certificate', 'cert.pem']], True) def test_get_best_match(self): - pass + target_name = 'www.eff.org' + names = [set(['www.eff.org', 'irrelevant.long.name.eff.org', '*.org']), + set(['eff.org', 'ww2.eff.org', 'test.www.eff.org']), + set(['*.eff.org', '.www.eff.org']), + set(['.eff.org', '*.org']), + set(['www.eff.', 'www.eff.*', '*.www.eff.org']), + set(['example.com', '~^(www\.)?(eff.+)', '*.eff.*']), + set(['*', '~^(www\.)?(eff.+)']), + set(['www.*', '~^(www\.)?(eff.+)', '.test.eff.org']), + set(['*.org', '*.eff.org', 'www.eff.*']), + set(['*.www.eff.org', 'www.*']), + set(['*.org']), + set([]), + set(['example.com'])] + winners = [('exact', 'www.eff.org'), + (None, None), + ('exact', '.www.eff.org'), + ('wildcard_start', '.eff.org'), + ('wildcard_end', 'www.eff.*'), + ('regex', '~^(www\.)?(eff.+)'), + ('wildcard_start', '*'), + ('wildcard_end', 'www.*'), + ('wildcard_start', '*.eff.org'), + ('wildcard_end', 'www.*'), + ('wildcard_start', '*.org'), + (None, None), + (None, None)] -# def test_find_dir(self): -# from letsencrypt.client.plugins.nginx.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_add_dir(self): -# aug_default = "/files" + self.parser.loc["default"] -# self.parser.add_dir(aug_default, "AddDirective", "test") -# -# self.assertTrue( -# self.parser.find_dir("AddDirective", "test", aug_default)) -# -# self.parser.add_dir(aug_default, "AddList", ["1", "2", "3", "4"]) -# matches = self.parser.find_dir("AddList", None, aug_default) -# for i, match in enumerate(matches): -# self.assertEqual(self.parser.aug.get(match), str(i + 1)) -# -# def test_add_dir_to_ifmodssl(self): -# """test add_dir_to_ifmodssl. -# -# Path must be valid before attempting to add to augeas -# -# """ -# from letsencrypt.client.plugins.nginx.parser import get_aug_path -# self.parser.add_dir_to_ifmodssl( -# get_aug_path(self.parser.loc["default"]), -# "FakeDirective", "123") -# -# matches = self.parser.find_dir("FakeDirective", "123") -# -# self.assertEqual(len(matches), 1) -# self.assertTrue("IfModule" in matches[0]) -# -# def test_get_aug_path(self): -# from letsencrypt.client.plugins.nginx.parser import get_aug_path -# self.assertEqual("/files/etc/nginx", get_aug_path("/etc/nginx")) -# -# def test_set_locations(self): -# with mock.patch("letsencrypt.client.plugins.nginx.parser." -# "os.path") as mock_path: -# -# mock_path.isfile.return_value = False -# -# # pylint: disable=protected-access -# self.assertRaises(errors.LetsEncryptConfiguratorError, -# self.parser._set_locations, self.ssl_options) -# -# mock_path.isfile.side_effect = [True, False, False] -# -# # pylint: disable=protected-access -# results = self.parser._set_locations(self.ssl_options) -# -# self.assertEqual(results["default"], results["listen"]) -# self.assertEqual(results["default"], results["name"]) + for i, winner in enumerate(winners): + self.assertEqual(winner, get_best_match(target_name, names[i])) if __name__ == "__main__": From d2588de4fdd40fbf288ce640f2161b3b0ba9d879 Mon Sep 17 00:00:00 2001 From: yan Date: Tue, 14 Apr 2015 16:24:10 -0700 Subject: [PATCH 23/38] Add get_all_certs_keys method to parser --- .../client/plugins/nginx/configurator.py | 18 +++----- letsencrypt/client/plugins/nginx/obj.py | 4 +- letsencrypt/client/plugins/nginx/parser.py | 29 ++++++++++++- .../client/plugins/nginx/tests/parser_test.py | 43 +++++++++++-------- .../tests/testdata/sites-enabled/example.com | 3 +- 5 files changed, 64 insertions(+), 33 deletions(-) diff --git a/letsencrypt/client/plugins/nginx/configurator.py b/letsencrypt/client/plugins/nginx/configurator.py index 48d44c5f8..d8c4e28ba 100644 --- a/letsencrypt/client/plugins/nginx/configurator.py +++ b/letsencrypt/client/plugins/nginx/configurator.py @@ -54,7 +54,7 @@ class NginxConfigurator(object): def __init__(self, config, version=None): """Initialize an Nginx Configurator. - :param tup version: version of Nginx as a tuple (2, 4, 7) + :param tup version: version of Nginx as a tuple (1, 4, 7) (used mostly for unittesting) """ @@ -133,6 +133,7 @@ class NginxConfigurator(object): ", ".join(str(addr) for addr in vhost.addrs))) self.save_notes += "\tssl_certificate %s\n" % cert self.save_notes += "\tssl_certificate_key %s\n" % key + self.save() ####################### # Vhost parsing methods @@ -272,23 +273,14 @@ class NginxConfigurator(object): def get_all_certs_keys(self): """Find all existing keys, certs from configuration. - Retrieve all certs and keys set in VirtualHosts on the Nginx 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 + :rtype: set """ - c_k = set() - - for vhost in self.vhosts: - if vhost.ssl: - # TODO: get the cert, key, and conf file paths - pass - - return c_k + return self.parser.get_all_certs_keys() ################################## # enhancement methods (IInstaller) @@ -453,6 +445,8 @@ class NginxConfigurator(object): if title and not temporary: self.reverter.finalize_checkpoint(title) + self.vhosts = self.parser.get_vhosts() + return True def recovery_routine(self): diff --git a/letsencrypt/client/plugins/nginx/obj.py b/letsencrypt/client/plugins/nginx/obj.py index 3eaee5a41..277dd81a1 100644 --- a/letsencrypt/client/plugins/nginx/obj.py +++ b/letsencrypt/client/plugins/nginx/obj.py @@ -99,13 +99,14 @@ class VirtualHost(object): # pylint: disable=too-few-public-methods :ivar set addrs: Virtual Host addresses (:class:`set` of :class:`Addr`) :ivar set names: Server names/aliases of vhost (:class:`list` of :class:`str`) + :ivar array raw: The raw form of the parsed server block :ivar bool ssl: SSLEngine on in vhost :ivar bool enabled: Virtual host is enabled """ - def __init__(self, filep, addrs, ssl, enabled, names): + def __init__(self, filep, addrs, ssl, enabled, names, raw): # pylint: disable=too-many-arguments """Initialize a VH.""" self.filep = filep @@ -113,6 +114,7 @@ class VirtualHost(object): # pylint: disable=too-few-public-methods self.names = names self.ssl = ssl self.enabled = enabled + self.raw = raw def __str__(self): addr_str = ", ".join(str(addr) for addr in self.addrs) diff --git a/letsencrypt/client/plugins/nginx/parser.py b/letsencrypt/client/plugins/nginx/parser.py index 52b02e9e1..fcd0d8919 100644 --- a/letsencrypt/client/plugins/nginx/parser.py +++ b/letsencrypt/client/plugins/nginx/parser.py @@ -98,7 +98,7 @@ class NginxParser(object): """ enabled = True # We only look at enabled vhosts for now vhosts = [] - servers = {} # Map of filename to list of parsed server blocks + servers = {} for filename in self.parsed: tree = self.parsed[filename] @@ -128,7 +128,8 @@ class NginxParser(object): parsed_server['addrs'], parsed_server['ssl'], enabled, - parsed_server['names']) + parsed_server['names'], + server) vhosts.append(vhost) return vhosts @@ -319,6 +320,30 @@ class NginxParser(object): lambda x: self._has_server_names(x, names), lambda x: x.extend(directives)) + def get_all_certs_keys(self): + """Gets all certs and keys in the nginx config. + + :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: set + + """ + c_k = set() + vhosts = self.get_vhosts() + for vhost in vhosts: + tup = [None, None, vhost.filep] + if vhost.ssl: + for directive in vhost.raw: + if directive[0] == 'ssl_certificate': + tup[0] = directive[1] + elif directive[0] == 'ssl_certificate_key': + tup[1] = directive[1] + if tup[0] is not None and tup[1] is not None: + c_k.add(tuple(tup)) + return c_k + def _do_for_subarray(entry, condition, func): """Executes a function for a subarray of a nested array if it matches diff --git a/letsencrypt/client/plugins/nginx/tests/parser_test.py b/letsencrypt/client/plugins/nginx/tests/parser_test.py index 55c7f5405..34a6eb04b 100644 --- a/letsencrypt/client/plugins/nginx/tests/parser_test.py +++ b/letsencrypt/client/plugins/nginx/tests/parser_test.py @@ -3,14 +3,9 @@ import glob import os import re import shutil -import sys import unittest -import zope.component - -from letsencrypt.client.display import util as display_util from letsencrypt.client.errors import LetsEncryptMisconfigurationError - from letsencrypt.client.plugins.nginx.nginxparser import dumps from letsencrypt.client.plugins.nginx.obj import Addr, VirtualHost from letsencrypt.client.plugins.nginx.parser import NginxParser, get_best_match @@ -23,9 +18,6 @@ class NginxParserTest(util.NginxTest): def setUp(self): super(NginxParserTest, self).setUp() - self.maxDiff = None - zope.component.provideUtility(display_util.FileDisplay(sys.stdout)) - def tearDown(self): shutil.rmtree(self.temp_dir) shutil.rmtree(self.config_dir) @@ -57,7 +49,8 @@ class NginxParserTest(util.NginxTest): set(parser.parsed.keys())) self.assertEqual([['server_name', 'somename alias another.alias']], parser.parsed[parser.abs_path('server.conf')]) - self.assertEqual([[['server'], [['listen', '9000'], + self.assertEqual([[['server'], [['listen', '69.50.225.155:9000'], + ['listen', '127.0.0.1'], ['server_name', '.example.com'], ['server_name', 'example.*']]]], parser.parsed[parser.abs_path( @@ -78,7 +71,8 @@ class NginxParserTest(util.NginxTest): self.assertEqual(3, len(glob.glob(parser.abs_path('*.test')))) self.assertEqual(2, len( glob.glob(parser.abs_path('sites-enabled/*.test')))) - self.assertEqual([[['server'], [['listen', '9000'], + self.assertEqual([[['server'], [['listen', '69.50.225.155:9000'], + ['listen', '127.0.0.1'], ['server_name', '.example.com'], ['server_name', 'example.*']]]], parsed[0]) @@ -89,21 +83,23 @@ class NginxParserTest(util.NginxTest): vhost1 = VirtualHost(parser.abs_path('nginx.conf'), [Addr('', '8080', False, False)], - False, True, set(['localhost'])) + False, True, set(['localhost']), []) vhost2 = VirtualHost(parser.abs_path('nginx.conf'), [Addr('somename', '8080', False, False), Addr('', '8000', False, False)], False, True, set(['somename', - 'another.alias', 'alias'])) + 'another.alias', 'alias']), []) vhost3 = VirtualHost(parser.abs_path('sites-enabled/example.com'), - [Addr('', '9000', False, False)], - False, True, set(['.example.com', 'example.*'])) + [Addr('69.50.225.155', '9000', False, False), + Addr('127.0.0.1', '', False, False)], + False, True, set(['.example.com', 'example.*']), + []) vhost4 = VirtualHost(parser.abs_path('sites-enabled/default'), [Addr('myhost', '', False, True)], - False, True, set(['www.example.org'])) + False, True, set(['www.example.org']), []) vhost5 = VirtualHost(parser.abs_path('foo.conf'), [Addr('*', '80', True, True)], - True, True, set(['*.www.foo.com'])) + True, True, set(['*.www.foo.com']), []) self.assertEqual(5, len(vhosts)) example_com = filter(lambda x: 'example.com' in x.filep, vhosts)[0] @@ -144,7 +140,9 @@ class NginxParserTest(util.NginxTest): filep, target, [['server_name', 'foo bar']], True) self.assertEqual( parser.parsed[filep], - [[['server'], [['listen', '9000'], ['server_name', 'foo bar'], + [[['server'], [['listen', '69.50.225.155:9000'], + ['listen', '127.0.0.1'], + ['server_name', 'foo bar'], ['server_name', 'foo bar']]]]) self.assertRaises(LetsEncryptMisconfigurationError, parser.add_server_directives, @@ -183,6 +181,17 @@ class NginxParserTest(util.NginxTest): for i, winner in enumerate(winners): self.assertEqual(winner, get_best_match(target_name, names[i])) + def test_get_all_certs_keys(self): + parser = NginxParser(self.config_path, self.ssl_options) + filep = parser.abs_path('sites-enabled/example.com') + parser.add_server_directives(filep, + set(['.example.com', 'example.*']), + [['ssl_certificate', 'foo.pem'], + ['ssl_certificate_key', 'bar.key'], + ['listen', '443 ssl']]) + ck = parser.get_all_certs_keys() + self.assertEqual(set([('foo.pem', 'bar.key', filep)]), ck) + if __name__ == "__main__": unittest.main() diff --git a/letsencrypt/client/plugins/nginx/tests/testdata/sites-enabled/example.com b/letsencrypt/client/plugins/nginx/tests/testdata/sites-enabled/example.com index bea8d7a3b..fd9117188 100644 --- a/letsencrypt/client/plugins/nginx/tests/testdata/sites-enabled/example.com +++ b/letsencrypt/client/plugins/nginx/tests/testdata/sites-enabled/example.com @@ -1,5 +1,6 @@ server { - listen 9000; + listen 69.50.225.155:9000; + listen 127.0.0.1; server_name .example.com; server_name example.*; } From 154db5a7577a5702c8b4af0b991cf55063f226f7 Mon Sep 17 00:00:00 2001 From: yan Date: Tue, 14 Apr 2015 16:43:40 -0700 Subject: [PATCH 24/38] Start adding tests for nginx configurator --- .../client/plugins/nginx/configurator.py | 12 +- letsencrypt/client/plugins/nginx/parser.py | 5 +- .../plugins/nginx/tests/configurator_test.py | 319 ++++++++++-------- .../plugins/nginx/tests/nginxparser_test.py | 4 +- .../client/plugins/nginx/tests/parser_test.py | 3 +- .../plugins/nginx/tests/testdata/foo.conf | 2 +- .../client/plugins/nginx/tests/util.py | 62 +--- 7 files changed, 201 insertions(+), 206 deletions(-) diff --git a/letsencrypt/client/plugins/nginx/configurator.py b/letsencrypt/client/plugins/nginx/configurator.py index d8c4e28ba..743d35b75 100644 --- a/letsencrypt/client/plugins/nginx/configurator.py +++ b/letsencrypt/client/plugins/nginx/configurator.py @@ -91,7 +91,7 @@ class NginxConfigurator(object): # Set Version if self.version is None: - self.version = self._get_version() + self.version = self.get_version() # Get all of the available vhosts self.vhosts = self.parser.get_vhosts() @@ -214,7 +214,7 @@ class NginxConfigurator(object): matches.append({'vhost': vhost, 'name': name, 'rank': 6 if vhost.ssl else 7}) - return sorted(matches, key=lambda x: x['rank'], reverse=True) + return sorted(matches, key=lambda x: x['rank']) def get_all_names(self): """Returns all names found in the Nginx Configuration. @@ -303,7 +303,7 @@ class NginxConfigurator(object): try: return self._enhance_func[enhancement]( self.choose_vhost(domain), options) - except ValueError: + except (KeyError, ValueError): raise errors.LetsEncryptConfiguratorError( "Unsupported enhancement: {}".format(enhancement)) except errors.LetsEncryptConfiguratorError: @@ -360,7 +360,7 @@ class NginxConfigurator(object): le_util.make_or_verify_dir(self.config.work_dir, 0o755, uid) le_util.make_or_verify_dir(self.config.backup_dir, 0o755, uid) - def _get_version(self): + def get_version(self): """Return version of Nginx Server. Version is returned as tuple. (ie. 2.4.7 = (2, 4, 7)) @@ -440,11 +440,11 @@ class NginxConfigurator(object): self.reverter.add_to_checkpoint(save_files, self.save_notes) - # Don't override original files for now. - self.parser.filedump('le') + self.parser.filedump(ext='') if title and not temporary: self.reverter.finalize_checkpoint(title) + # Refresh the vhosts self.vhosts = self.parser.get_vhosts() return True diff --git a/letsencrypt/client/plugins/nginx/parser.py b/letsencrypt/client/plugins/nginx/parser.py index fcd0d8919..ff9a96a59 100644 --- a/letsencrypt/client/plugins/nginx/parser.py +++ b/letsencrypt/client/plugins/nginx/parser.py @@ -169,10 +169,11 @@ class NginxParser(object): names = re.sub(whitespace_re, ' ', names) return names.split(' ') - def _parse_files(self, filepath): + def _parse_files(self, filepath, override=False): """Parse files from a glob :param str filepath: Nginx config file path + :param bool override: Whether to parse a file that has been parsed :returns: list of parsed tree structures :rtype: list @@ -180,7 +181,7 @@ class NginxParser(object): files = glob.glob(filepath) trees = [] for f in files: - if f in self.parsed: + if f in self.parsed and not override: continue try: with open(f) as fo: diff --git a/letsencrypt/client/plugins/nginx/tests/configurator_test.py b/letsencrypt/client/plugins/nginx/tests/configurator_test.py index 6b2612616..913efbc48 100644 --- a/letsencrypt/client/plugins/nginx/tests/configurator_test.py +++ b/letsencrypt/client/plugins/nginx/tests/configurator_test.py @@ -1,6 +1,4 @@ """Test for letsencrypt.client.plugins.nginx.configurator.""" -import os -import re import shutil import unittest @@ -8,145 +6,151 @@ import mock from letsencrypt.acme import challenges -from letsencrypt.client import achallenges from letsencrypt.client import errors -from letsencrypt.client import le_util - -from letsencrypt.client.plugins.nginx import configurator -from letsencrypt.client.plugins.nginx import obj -from letsencrypt.client.plugins.nginx import parser from letsencrypt.client.plugins.nginx.tests import util -class TwoVhost80Test(util.NginxTest): - """Test two standard well configured HTTP vhosts.""" +class NginxConfiguratorTest(util.NginxTest): + """Test a semi complex vhost configuration.""" def setUp(self): - super(TwoVhost80Test, self).setUp() + super(NginxConfiguratorTest, self).setUp() - with mock.patch("letsencrypt.client.plugins.nginx.configurator." - "mod_loaded") as mock_load: - mock_load.return_value = True - self.config = util.get_nginx_configurator( - self.config_path, self.config_dir, self.work_dir, - self.ssl_options) - - self.vh_truth = util.get_vh_truth( - self.temp_dir, "debian_nginx_2_4/two_vhost_80") + self.config = util.get_nginx_configurator( + self.config_path, self.config_dir, self.work_dir, + self.ssl_options) def tearDown(self): shutil.rmtree(self.temp_dir) shutil.rmtree(self.config_dir) shutil.rmtree(self.work_dir) + def test_prepare(self): + self.assertEquals((1, 6, 2), self.config.version) + self.assertEquals(5, len(self.config.vhosts)) + def test_get_all_names(self): names = self.config.get_all_names() self.assertEqual(names, set( - ["letsencrypt.demo", "encryption-example.demo", "ip-172-30-0-17"])) + ["*.www.foo.com", "somename", "another.alias", + "alias", "localhost", ".example.com", + "155.225.50.69.nephoscale.net", "*.www.example.com", + "example.*", "www.example.org", "myhost"])) - def test_get_virtual_hosts(self): - """Make sure all vhosts are being properly found. + def test_supported_enhancements(self): + self.assertEqual([], self.config.supported_enhancements()) - .. note:: If test fails, only finding 1 Vhost... it is likely that - it is a problem with is_enabled. + def test_enhance(self): + self.assertRaises(errors.LetsEncryptConfiguratorError, + self.config.enhance, + 'myhost', + 'redirect') - """ - vhs = self.config.get_virtual_hosts() - self.assertEqual(len(vhs), 4) - found = 0 + def test_get_chall_pref(self): + self.assertEqual([challenges.DVSNI], + self.config.get_chall_pref('myhost')) - for vhost in vhs: - for truth in self.vh_truth: - if vhost == truth: - found += 1 - break - - self.assertEqual(found, 4) - - def test_is_site_enabled(self): - """Test if site is enabled. - - .. note:: This test currently fails for hard links - (which may happen if you move dirs incorrectly) - .. warning:: This test does not work when running using the - unittest.main() function. It incorrectly copies symlinks. - - """ - self.assertTrue(self.config.is_site_enabled(self.vh_truth[0].filep)) - self.assertFalse(self.config.is_site_enabled(self.vh_truth[1].filep)) - self.assertTrue(self.config.is_site_enabled(self.vh_truth[2].filep)) - self.assertTrue(self.config.is_site_enabled(self.vh_truth[3].filep)) - - 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") + def test_save(self): + filep = self.config.parser.abs_path('sites-enabled/example.com') + self.config.parser.add_server_directives( + filep, set(['.example.com', 'example.*']), + [['listen', '443 ssl']]) 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) + # pylint: disable=protected-access + parsed = self.config.parser._parse_files(filep, override=True) + self.assertEqual([[['server'], [['listen', '69.50.225.155:9000'], + ['listen', '127.0.0.1'], + ['server_name', '.example.com'], + ['server_name', 'example.*'], + ['listen', '443 ssl']]]], + parsed[0]) - # 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) + def test_choose_vhost(self): + localhost_conf = set(['localhost']) + server_conf = set(['somename', 'another.alias', 'alias']) + example_conf = set(['.example.com', 'example.*']) + foo_conf = set(['*.www.foo.com', '*.www.example.com']) - self.assertEqual(len(loc_key), 1) - self.assertEqual(configurator.get_file_path(loc_key[0]), - self.vh_truth[1].filep) + results = {'localhost': localhost_conf, + 'alias': server_conf, + 'example.com': example_conf, + 'example.com.uk.test': example_conf, + 'www.example.com': example_conf, + 'test.www.example.com': foo_conf, + 'abc.www.foo.com': foo_conf} + bad_results = ['www.foo.com', 'example', '69.255.225.155'] - self.assertEqual(len(loc_chain), 1) - self.assertEqual(configurator.get_file_path(loc_chain[0]), - self.vh_truth[1].filep) + for name in results: + self.assertEqual(results[name], + self.config.choose_vhost(name).names) + for name in bad_results: + self.assertEqual(None, self.config.choose_vhost(name)) - def test_is_name_vhost(self): - 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_more_info(self): + self.assertTrue('nginx.conf' in self.config.more_info()) - def test_add_name_vhost(self): - self.config.add_name_vhost("*:443") - self.assertTrue(self.config.parser.find_dir( - "NameVirtualHost", re.escape("*:443"))) + def test_deploy_cert(self): + pass + # 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 test_make_vhost_ssl(self): - ssl_vhost = self.config.make_vhost_ssl(self.vh_truth[0]) - - self.assertEqual( - ssl_vhost.filep, - os.path.join(self.config_path, "sites-available", - "encryption-example-le-ssl.conf")) - - self.assertEqual(ssl_vhost.path, - "/files" + ssl_vhost.filep + "/IfModule/VirtualHost") - self.assertEqual(len(ssl_vhost.addrs), 1) - self.assertEqual(set([obj.Addr.fromstring("*:443")]), ssl_vhost.addrs) - self.assertEqual(ssl_vhost.names, set(["encryption-example.demo"])) - self.assertTrue(ssl_vhost.ssl) - self.assertFalse(ssl_vhost.enabled) - - self.assertTrue(self.config.parser.find_dir( - "SSLCertificateFile", None, ssl_vhost.path)) - self.assertTrue(self.config.parser.find_dir( - "SSLCertificateKeyFile", None, ssl_vhost.path)) - 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]), - self.config.is_name_vhost(ssl_vhost)) - - self.assertEqual(len(self.config.vhosts), 5) + pass +# ssl_vhost = self.config.make_vhost_ssl(self.vh_truth[0]) +# +# self.assertEqual( +# ssl_vhost.filep, +# os.path.join(self.config_path, "sites-available", +# "encryption-example-le-ssl.conf")) +# +# self.assertEqual(ssl_vhost.path, +# "/files" + ssl_vhost.filep + "/IfModule/VirtualHost") +# self.assertEqual(len(ssl_vhost.addrs), 1) +# self.assertEqual(set([obj.Addr.fromstring("*:443")]), ssl_vhost.addrs) +# self.assertEqual(ssl_vhost.names, set(["encryption-example.demo"])) +# self.assertTrue(ssl_vhost.ssl) +# self.assertFalse(ssl_vhost.enabled) +# +# self.assertTrue(self.config.parser.find_dir( +# "SSLCertificateFile", None, ssl_vhost.path)) +# self.assertTrue(self.config.parser.find_dir( +# "SSLCertificateKeyFile", None, ssl_vhost.path)) +# 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]), +# self.config.is_name_vhost(ssl_vhost)) +# +# self.assertEqual(len(self.config.vhosts), 5) @mock.patch("letsencrypt.client.plugins.nginx.configurator." "dvsni.NginxDvsni.perform") @@ -155,56 +159,81 @@ class TwoVhost80Test(util.NginxTest): def test_perform(self, mock_restart, mock_dvsni_perform): # Only tests functionality specific to configurator.perform # Note: As more challenges are offered this will have to be expanded - auth_key = le_util.Key(self.rsa256_file, self.rsa256_pem) - achall1 = achallenges.DVSNI( - chall=challenges.DVSNI( - r="jIq_Xy1mXGN37tb4L6Xj_es58fW571ZNyXekdZzhh7Q", - nonce="37bc5eb75d3e00a19b4f6355845e5a18"), - domain="encryption-example.demo", key=auth_key) - achall2 = achallenges.DVSNI( - chall=challenges.DVSNI( - r="uqnaPzxtrndteOqtrXb0Asl5gOJfWAnnx6QJyvcmlDU", - nonce="59ed014cac95f77057b1d7a1b2c596ba"), - domain="letsencrypt.demo", key=auth_key) - - dvsni_ret_val = [ - challenges.DVSNIResponse(s="randomS1"), - challenges.DVSNIResponse(s="randomS2"), - ] - - mock_dvsni_perform.return_value = dvsni_ret_val - responses = self.config.perform([achall1, achall2]) - - self.assertEqual(mock_dvsni_perform.call_count, 1) - self.assertEqual(responses, dvsni_ret_val) - - self.assertEqual(mock_restart.call_count, 1) + pass +# auth_key = le_util.Key(self.rsa256_file, self.rsa256_pem) +# achall1 = achallenges.DVSNI( +# chall=challenges.DVSNI( +# r="jIq_Xy1mXGN37tb4L6Xj_es58fW571ZNyXekdZzhh7Q", +# nonce="37bc5eb75d3e00a19b4f6355845e5a18"), +# domain="encryption-example.demo", key=auth_key) +# achall2 = achallenges.DVSNI( +# chall=challenges.DVSNI( +# r="uqnaPzxtrndteOqtrXb0Asl5gOJfWAnnx6QJyvcmlDU", +# nonce="59ed014cac95f77057b1d7a1b2c596ba"), +# domain="letsencrypt.demo", key=auth_key) +# +# dvsni_ret_val = [ +# challenges.DVSNIResponse(s="randomS1"), +# challenges.DVSNIResponse(s="randomS2"), +# ] +# +# mock_dvsni_perform.return_value = dvsni_ret_val +# responses = self.config.perform([achall1, achall2]) +# +# self.assertEqual(mock_dvsni_perform.call_count, 1) +# self.assertEqual(responses, dvsni_ret_val) +# +# self.assertEqual(mock_restart.call_count, 1) @mock.patch("letsencrypt.client.plugins.nginx.configurator." "subprocess.Popen") def test_get_version(self, mock_popen): mock_popen().communicate.return_value = ( - "Server Version: Nginx/2.4.2 (Debian)", "") - self.assertEqual(self.config.get_version(), (2, 4, 2)) + "", "\n".join(["nginx version: nginx/1.4.2", + "built by clang 6.0 (clang-600.0.56)" + " (based on LLVM 3.5svn)", + "TLS SNI support enabled", + "configure arguments: --prefix=/usr/local/Cellar/" + "nginx/1.6.2 --with-http_ssl_module"])) + self.assertEqual(self.config.get_version(), (1, 4, 2)) mock_popen().communicate.return_value = ( - "Server Version: Nginx/2 (Linux)", "") - self.assertEqual(self.config.get_version(), (2,)) + "", "\n".join(["blah 0.0.1", + "TLS SNI support enabled"])) + self.assertRaises(errors.LetsEncryptConfiguratorError, + self.config.get_version) mock_popen().communicate.return_value = ( - "Server Version: Nginx (Debian)", "") - self.assertRaises( - errors.LetsEncryptConfiguratorError, self.config.get_version) + "", "\n".join(["nginx version: nginx/1.4.2", + ""])) + self.assertRaises(errors.LetsEncryptConfiguratorError, + self.config.get_version) mock_popen().communicate.return_value = ( - "Server Version: Nginx/2.3{0} Nginx/2.4.7".format(os.linesep), "") - self.assertRaises( - errors.LetsEncryptConfiguratorError, self.config.get_version) + "", "\n".join(["nginx version: nginx/0.8.1", + ""])) + self.assertRaises(errors.LetsEncryptConfiguratorError, + self.config.get_version) mock_popen.side_effect = OSError("Can't find program") self.assertRaises( errors.LetsEncryptConfiguratorError, self.config.get_version) + @mock.patch("letsencrypt.client.plugins.nginx.configurator." + "subprocess.Popen") + def test_nginx_restart(self, mock_popen): + m = mock_popen() + m.communicate.return_value = ('', '') + m.returncode = 0 + self.assertTrue(self.config.restart()) + + @mock.patch("letsencrypt.client.plugins.nginx.configurator." + "subprocess.Popen") + def test_config_test(self, mock_popen): + m = mock_popen() + m.communicate.return_value = ('', '') + m.returncode = 0 + self.assertTrue(self.config.config_test()) if __name__ == "__main__": unittest.main() diff --git a/letsencrypt/client/plugins/nginx/tests/nginxparser_test.py b/letsencrypt/client/plugins/nginx/tests/nginxparser_test.py index 00ea9e6c5..48f7590db 100644 --- a/letsencrypt/client/plugins/nginx/tests/nginxparser_test.py +++ b/letsencrypt/client/plugins/nginx/tests/nginxparser_test.py @@ -64,8 +64,8 @@ class TestRawNginxParser(unittest.TestCase): parsed, [['user', 'www-data'], [['server'], [ - ['listen', '80'], - ['server_name', 'foo.com'], + ['listen', '*:80 default_server ssl'], + ['server_name', '*.www.foo.com *.www.example.com'], ['root', '/home/ubuntu/sites/foo/'], [['location', '/status'], [ ['check_status'], diff --git a/letsencrypt/client/plugins/nginx/tests/parser_test.py b/letsencrypt/client/plugins/nginx/tests/parser_test.py index 34a6eb04b..36c3ed2e0 100644 --- a/letsencrypt/client/plugins/nginx/tests/parser_test.py +++ b/letsencrypt/client/plugins/nginx/tests/parser_test.py @@ -99,7 +99,8 @@ class NginxParserTest(util.NginxTest): False, True, set(['www.example.org']), []) vhost5 = VirtualHost(parser.abs_path('foo.conf'), [Addr('*', '80', True, True)], - True, True, set(['*.www.foo.com']), []) + True, True, set(['*.www.foo.com', + '*.www.example.com']), []) self.assertEqual(5, len(vhosts)) example_com = filter(lambda x: 'example.com' in x.filep, vhosts)[0] diff --git a/letsencrypt/client/plugins/nginx/tests/testdata/foo.conf b/letsencrypt/client/plugins/nginx/tests/testdata/foo.conf index 56ae5b33c..774334220 100644 --- a/letsencrypt/client/plugins/nginx/tests/testdata/foo.conf +++ b/letsencrypt/client/plugins/nginx/tests/testdata/foo.conf @@ -3,7 +3,7 @@ user www-data; server { listen *:80 default_server ssl; - server_name *.www.foo.com; + server_name *.www.foo.com *.www.example.com; root /home/ubuntu/sites/foo/; location /status { diff --git a/letsencrypt/client/plugins/nginx/tests/util.py b/letsencrypt/client/plugins/nginx/tests/util.py index e8467502e..205e511af 100644 --- a/letsencrypt/client/plugins/nginx/tests/util.py +++ b/letsencrypt/client/plugins/nginx/tests/util.py @@ -9,13 +9,13 @@ import mock from letsencrypt.client import constants from letsencrypt.client.plugins.nginx import configurator -from letsencrypt.client.plugins.nginx import obj class NginxTest(unittest.TestCase): # pylint: disable=too-few-public-methods def setUp(self): super(NginxTest, self).setUp() + self.maxDiff = None self.temp_dir, self.config_dir, self.work_dir = dir_setup( "testdata") @@ -59,59 +59,23 @@ def setup_nginx_ssl_options(config_dir): def get_nginx_configurator( - config_path, config_dir, work_dir, ssl_options, version=(2, 4, 7)): + config_path, config_dir, work_dir, ssl_options, version=(1, 6, 2)): """Create an Nginx Configurator with the specified options.""" backups = os.path.join(work_dir, "backups") - with mock.patch("letsencrypt.client.plugins.nginx.configurator." - "subprocess.Popen") as mock_popen: - # This just states that the ssl module is already loaded - mock_popen().communicate.return_value = ("ssl_module", "") - config = configurator.NginxConfigurator( - mock.MagicMock( - nginx_server_root=config_path, - nginx_mod_ssl_conf=ssl_options, - le_vhost_ext="-le-ssl.conf", - backup_dir=backups, - config_dir=config_dir, - temp_checkpoint_dir=os.path.join(work_dir, "temp_checkpoints"), - in_progress_dir=os.path.join(backups, "IN_PROGRESS"), - work_dir=work_dir), - version) + config = configurator.NginxConfigurator( + mock.MagicMock( + nginx_server_root=config_path, + nginx_mod_ssl_conf=ssl_options, + le_vhost_ext="-le-ssl.conf", + backup_dir=backups, + config_dir=config_dir, + temp_checkpoint_dir=os.path.join(work_dir, "temp_checkpoints"), + in_progress_dir=os.path.join(backups, "IN_PROGRESS"), + work_dir=work_dir), + version) config.prepare() return config - - -def get_vh_truth(temp_dir, config_name): - """Return the ground truth for the specified directory.""" - if config_name == "debian_nginx_2_4/two_vhost_80": - prefix = os.path.join( - temp_dir, config_name, "nginx2/sites-available") - aug_pre = "/files" + prefix - vh_truth = [ - obj.VirtualHost( - os.path.join(prefix, "encryption-example.conf"), - os.path.join(aug_pre, "encryption-example.conf/VirtualHost"), - set([obj.Addr.fromstring("*:80")]), - False, True, set(["encryption-example.demo"])), - obj.VirtualHost( - os.path.join(prefix, "default-ssl.conf"), - os.path.join(aug_pre, "default-ssl.conf/IfModule/VirtualHost"), - set([obj.Addr.fromstring("_default_:443")]), True, False), - obj.VirtualHost( - os.path.join(prefix, "000-default.conf"), - os.path.join(aug_pre, "000-default.conf/VirtualHost"), - set([obj.Addr.fromstring("*:80")]), False, True, - set(["ip-172-30-0-17"])), - obj.VirtualHost( - os.path.join(prefix, "letsencrypt.conf"), - os.path.join(aug_pre, "letsencrypt.conf/VirtualHost"), - set([obj.Addr.fromstring("*:80")]), False, True, - set(["letsencrypt.demo"])), - ] - return vh_truth - - return None From eeb81cbf1fc554ff8a26be6142b4a40fdc2c0565 Mon Sep 17 00:00:00 2001 From: yan Date: Wed, 15 Apr 2015 14:44:51 -0700 Subject: [PATCH 25/38] Remove vhosts instance variable For now, rebuild vhosts from parser.parsed on every invocation to ensure that vhosts is up-to-date with parser.parsed. --- .../client/plugins/nginx/configurator.py | 46 ++++++------------- letsencrypt/client/plugins/nginx/parser.py | 6 +++ .../plugins/nginx/tests/configurator_test.py | 2 +- .../client/plugins/nginx/tests/parser_test.py | 3 +- 4 files changed, 24 insertions(+), 33 deletions(-) diff --git a/letsencrypt/client/plugins/nginx/configurator.py b/letsencrypt/client/plugins/nginx/configurator.py index 743d35b75..4ac36dcc1 100644 --- a/letsencrypt/client/plugins/nginx/configurator.py +++ b/letsencrypt/client/plugins/nginx/configurator.py @@ -40,11 +40,6 @@ class NginxConfigurator(object): :type reverter: :class:`letsencrypt.client.reverter.Reverter` :ivar tup version: version of Nginx - :ivar list vhosts: All vhosts found in the configuration - (:class:`list` of - :class:`~letsencrypt.client.plugins.nginx.obj.VirtualHost`) - - :ivar dict assoc: Mapping between domains and vhosts """ zope.interface.implements(interfaces.IAuthenticator, interfaces.IInstaller) @@ -67,15 +62,12 @@ class NginxConfigurator(object): # Files to save self.save_notes = "" - # Add name_server association dict - self.assoc = dict() # Add number of outstanding challenges self._chall_out = 0 # These will be set in the prepare function self.parser = None self.version = version - self.vhosts = None self._enhance_func = {} # TODO: Support at least redirects # Set up reverter @@ -93,9 +85,6 @@ class NginxConfigurator(object): if self.version is None: self.version = self.get_version() - # Get all of the available vhosts - self.vhosts = self.parser.get_vhosts() - temp_install(self.config.nginx_mod_ssl_conf) # Entry point in main.py for installing cert @@ -157,24 +146,19 @@ class NginxConfigurator(object): """ vhost = None - # If we already found the vhost for the target, use it - if target_name in self.assoc: - vhost = self.assoc[target_name] + matches = self._get_ranked_matches(target_name) + if len(matches) == 0: + # No matches at all :'( + pass + elif matches[0]['rank'] in range(2, 6): + # Wildcard match - need to find the longest one + rank = matches[0]['rank'] + wildcards = [x for x in matches if x['rank'] == rank] + vhost = max(wildcards, key=lambda x: len(x['name']))['vhost'] else: - matches = self._get_ranked_matches(target_name) - if len(matches) == 0: - # No matches at all :'( - pass - elif matches[0]['rank'] in range(2, 6): - # Wildcard match - need to find the longest one - rank = matches[0]['rank'] - wildcards = [x for x in matches if x['rank'] == rank] - vhost = max(wildcards, key=lambda x: len(x['name']))['vhost'] - else: - vhost = matches[0]['vhost'] + vhost = matches[0]['vhost'] if vhost is not None: - self.assoc[target_name] = vhost if not vhost.ssl: self._make_server_ssl(vhost.filep, vhost.names) @@ -196,7 +180,7 @@ class NginxConfigurator(object): # 3. longest wildcard name ending with * # 4. first matching regex in order of appearance in the file matches = [] - for vhost in self.vhosts: + for vhost in self.parser.get_vhosts(): name_type, name = parser.get_best_match(target_name, vhost.names) if name_type == 'exact': matches.append({'vhost': vhost, @@ -233,7 +217,7 @@ class NginxConfigurator(object): hostname_regex = r"^(([a-z0-9]|[a-z0-9][a-z0-9\-]*[a-z0-9])\.)*[a-z]+$" hostnames = re.compile(hostname_regex, re.IGNORECASE) - for vhost in self.vhosts: + for vhost in self.parser.get_vhosts(): all_names.update(vhost.names) for addr in vhost.addrs: @@ -444,9 +428,6 @@ class NginxConfigurator(object): if title and not temporary: self.reverter.finalize_checkpoint(title) - # Refresh the vhosts - self.vhosts = self.parser.get_vhosts() - return True def recovery_routine(self): @@ -456,10 +437,12 @@ class NginxConfigurator(object): """ self.reverter.recovery_routine() + self.parser.load() def revert_challenge_config(self): """Used to cleanup challenge configurations.""" self.reverter.revert_temporary_config() + self.parser.load() def rollback_checkpoints(self, rollback=1): """Rollback saved checkpoints. @@ -468,6 +451,7 @@ class NginxConfigurator(object): """ self.reverter.rollback_checkpoints(rollback) + self.parser.load() def view_config_changes(self): """Show all of the configuration changes that have taken place.""" diff --git a/letsencrypt/client/plugins/nginx/parser.py b/letsencrypt/client/plugins/nginx/parser.py index ff9a96a59..fdb4afeec 100644 --- a/letsencrypt/client/plugins/nginx/parser.py +++ b/letsencrypt/client/plugins/nginx/parser.py @@ -27,6 +27,12 @@ class NginxParser(object): # Parse nginx.conf and included files. # TODO: Check sites-available/ as well. For now, the configurator does # not enable sites from there. + self.load() + + def load(self): + """Loads Nginx files into a parsed tree. + + """ self._parse_recursively(self.loc["root"]) def _parse_recursively(self, filepath): diff --git a/letsencrypt/client/plugins/nginx/tests/configurator_test.py b/letsencrypt/client/plugins/nginx/tests/configurator_test.py index 913efbc48..d0525e740 100644 --- a/letsencrypt/client/plugins/nginx/tests/configurator_test.py +++ b/letsencrypt/client/plugins/nginx/tests/configurator_test.py @@ -28,7 +28,7 @@ class NginxConfiguratorTest(util.NginxTest): def test_prepare(self): self.assertEquals((1, 6, 2), self.config.version) - self.assertEquals(5, len(self.config.vhosts)) + self.assertEquals(5, len(self.config.parser.parsed)) def test_get_all_names(self): names = self.config.get_all_names() diff --git a/letsencrypt/client/plugins/nginx/tests/parser_test.py b/letsencrypt/client/plugins/nginx/tests/parser_test.py index 36c3ed2e0..d1bc39af6 100644 --- a/letsencrypt/client/plugins/nginx/tests/parser_test.py +++ b/letsencrypt/client/plugins/nginx/tests/parser_test.py @@ -37,11 +37,12 @@ class NginxParserTest(util.NginxTest): parser = NginxParser(self.config_path + os.path.sep, None) self.assertEqual(parser.root, self.config_path) - def test_parse(self): + def test_load(self): """Test recursive conf file parsing. """ parser = NginxParser(self.config_path, self.ssl_options) + parser.load() self.assertEqual(set(map(parser.abs_path, ['foo.conf', 'nginx.conf', 'server.conf', 'sites-enabled/default', From f050fcfa580cd435edf4702bf02f493d02cf8dff Mon Sep 17 00:00:00 2001 From: yan Date: Wed, 15 Apr 2015 23:11:35 -0700 Subject: [PATCH 26/38] Add unit test for deploying cert --- .../client/plugins/nginx/configurator.py | 4 +- letsencrypt/client/plugins/nginx/dvsni.py | 3 + letsencrypt/client/plugins/nginx/parser.py | 37 ++-- .../plugins/nginx/tests/configurator_test.py | 167 +++++++++--------- 4 files changed, 116 insertions(+), 95 deletions(-) diff --git a/letsencrypt/client/plugins/nginx/configurator.py b/letsencrypt/client/plugins/nginx/configurator.py index 4ac36dcc1..38006e742 100644 --- a/letsencrypt/client/plugins/nginx/configurator.py +++ b/letsencrypt/client/plugins/nginx/configurator.py @@ -95,6 +95,8 @@ class NginxConfigurator(object): Nginx doesn't have a cert chain directive, so the last parameter is always ignored. It expects the cert file to have the concatenated chain. + .. note:: This doesn't save the config files! + :param str domain: domain to deploy certificate :param str cert: certificate filename :param str key: private key filename @@ -122,7 +124,6 @@ class NginxConfigurator(object): ", ".join(str(addr) for addr in vhost.addrs))) self.save_notes += "\tssl_certificate %s\n" % cert self.save_notes += "\tssl_certificate_key %s\n" % key - self.save() ####################### # Vhost parsing methods @@ -424,6 +425,7 @@ class NginxConfigurator(object): self.reverter.add_to_checkpoint(save_files, self.save_notes) + # Change 'ext' to something else to not override existing conf files self.parser.filedump(ext='') if title and not temporary: self.reverter.finalize_checkpoint(title) diff --git a/letsencrypt/client/plugins/nginx/dvsni.py b/letsencrypt/client/plugins/nginx/dvsni.py index c20ce1c0e..504f2c179 100644 --- a/letsencrypt/client/plugins/nginx/dvsni.py +++ b/letsencrypt/client/plugins/nginx/dvsni.py @@ -8,6 +8,9 @@ from letsencrypt.client.plugins.nginx import parser class NginxDvsni(object): """Class performs DVSNI challenges within the Nginx configurator. + .. todo:: This is basically copied-and-pasted from the Apache equivalent. + It doesn't actually work yet. + :ivar configurator: NginxConfigurator object :type configurator: :class:`~nginx.configurator.NginxConfigurator` diff --git a/letsencrypt/client/plugins/nginx/parser.py b/letsencrypt/client/plugins/nginx/parser.py index fdb4afeec..1e31f68cf 100644 --- a/letsencrypt/client/plugins/nginx/parser.py +++ b/letsencrypt/client/plugins/nginx/parser.py @@ -115,16 +115,9 @@ class NginxParser(object): lambda x: servers[filename].append(x[1])) # Find 'include' statements in server blocks and append their trees - for server in servers[filename]: - for directive in server: - if (self._is_include_directive(directive)): - included_files = glob.glob( - self.abs_path(directive[1])) - for f in included_files: - try: - server.extend(self.parsed[f]) - except: - pass + for i, server in enumerate(servers[filename]): + new_server = self._get_included_directives(server) + servers[filename][i] = new_server for filename in servers: for server in servers[filename]: @@ -140,6 +133,26 @@ class NginxParser(object): return vhosts + def _get_included_directives(self, block): + """Returns array with the "include" directives expanded out by + concatenating the contents of the included file to the block. + + :param list block: + :rtype: list + + """ + result = list(block) # Copy the list to keep self.parsed idempotent + for directive in block: + if (self._is_include_directive(directive)): + included_files = glob.glob( + self.abs_path(directive[1])) + for f in included_files: + try: + result.extend(self.parsed[f]) + except: + pass + return result + def _parse_server(self, server): """Parses a list of server directives. @@ -270,8 +283,9 @@ class NginxParser(object): # Can't be a server block return False + new_entry = self._get_included_directives(entry) server_names = set() - for item in entry: + for item in new_entry: if type(item) != list: # Can't be a server block return False @@ -323,6 +337,7 @@ class NginxParser(object): lambda x: self._has_server_names(x, names), lambda x: self._replace_directives(x, directives)) else: + print('adding server directives for %s' % filename) _do_for_subarray(self.parsed[filename], lambda x: self._has_server_names(x, names), lambda x: x.extend(directives)) diff --git a/letsencrypt/client/plugins/nginx/tests/configurator_test.py b/letsencrypt/client/plugins/nginx/tests/configurator_test.py index d0525e740..bf74569eb 100644 --- a/letsencrypt/client/plugins/nginx/tests/configurator_test.py +++ b/letsencrypt/client/plugins/nginx/tests/configurator_test.py @@ -6,7 +6,9 @@ import mock from letsencrypt.acme import challenges +from letsencrypt.client import achallenges from letsencrypt.client import errors +from letsencrypt.client import le_util from letsencrypt.client.plugins.nginx.tests import util @@ -92,65 +94,66 @@ class NginxConfiguratorTest(util.NginxTest): self.assertTrue('nginx.conf' in self.config.more_info()) def test_deploy_cert(self): - pass - # 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) + server_conf = self.config.parser.abs_path('server.conf') + nginx_conf = self.config.parser.abs_path('nginx.conf') + example_conf = self.config.parser.abs_path('sites-enabled/example.com') - def test_make_vhost_ssl(self): - pass -# ssl_vhost = self.config.make_vhost_ssl(self.vh_truth[0]) -# -# self.assertEqual( -# ssl_vhost.filep, -# os.path.join(self.config_path, "sites-available", -# "encryption-example-le-ssl.conf")) -# -# self.assertEqual(ssl_vhost.path, -# "/files" + ssl_vhost.filep + "/IfModule/VirtualHost") -# self.assertEqual(len(ssl_vhost.addrs), 1) -# self.assertEqual(set([obj.Addr.fromstring("*:443")]), ssl_vhost.addrs) -# self.assertEqual(ssl_vhost.names, set(["encryption-example.demo"])) -# self.assertTrue(ssl_vhost.ssl) -# self.assertFalse(ssl_vhost.enabled) -# -# self.assertTrue(self.config.parser.find_dir( -# "SSLCertificateFile", None, ssl_vhost.path)) -# self.assertTrue(self.config.parser.find_dir( -# "SSLCertificateKeyFile", None, ssl_vhost.path)) -# 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]), -# self.config.is_name_vhost(ssl_vhost)) -# -# self.assertEqual(len(self.config.vhosts), 5) + # Get the default 443 vhost + self.config.deploy_cert( + "www.example.com", + "example/cert.pem", "example/key.pem") + self.config.deploy_cert( + "another.alias", + "/etc/nginx/cert.pem", "/etc/nginx/key.pem") + self.config.save() + + self.config.parser.load() + + self.assertEqual([[['server'], + [['listen', '69.50.225.155:9000'], + ['listen', '127.0.0.1'], + ['server_name', '.example.com'], + ['server_name', 'example.*'], + ['listen', '443 ssl'], + ['ssl_certificate', 'example/cert.pem'], + ['ssl_certificate_key', 'example/key.pem'], + ['include', + self.config.parser.loc["ssl_options"]]]]], + self.config.parser.parsed[example_conf]) + self.assertEqual([['server_name', 'somename alias another.alias']], + self.config.parser.parsed[server_conf]) + self.assertEqual([['server'], + [['listen', '8000'], + ['listen', 'somename:8080'], + ['include', 'server.conf'], + [['location', '/'], + [['root', 'html'], + ['index', 'index.html index.htm']]], + ['listen', '443 ssl'], + ['ssl_certificate', '/etc/nginx/cert.pem'], + ['ssl_certificate_key', '/etc/nginx/key.pem'], + ['include', + self.config.parser.loc["ssl_options"]]]], + self.config.parser.parsed[nginx_conf][-1][-1][-3]) + + def test_get_all_certs_keys(self): + nginx_conf = self.config.parser.abs_path('nginx.conf') + example_conf = self.config.parser.abs_path('sites-enabled/example.com') + + # Get the default 443 vhost + self.config.deploy_cert( + "www.example.com", + "example/cert.pem", "example/key.pem") + self.config.deploy_cert( + "another.alias", + "/etc/nginx/cert.pem", "/etc/nginx/key.pem") + self.config.save() + + self.config.parser.load() + self.assertEqual(set([ + ('example/cert.pem', 'example/key.pem', example_conf), + ('/etc/nginx/cert.pem', '/etc/nginx/key.pem', nginx_conf), + ]), self.config.get_all_certs_keys()) @mock.patch("letsencrypt.client.plugins.nginx.configurator." "dvsni.NginxDvsni.perform") @@ -159,31 +162,29 @@ class NginxConfiguratorTest(util.NginxTest): def test_perform(self, mock_restart, mock_dvsni_perform): # Only tests functionality specific to configurator.perform # Note: As more challenges are offered this will have to be expanded - pass -# auth_key = le_util.Key(self.rsa256_file, self.rsa256_pem) -# achall1 = achallenges.DVSNI( -# chall=challenges.DVSNI( -# r="jIq_Xy1mXGN37tb4L6Xj_es58fW571ZNyXekdZzhh7Q", -# nonce="37bc5eb75d3e00a19b4f6355845e5a18"), -# domain="encryption-example.demo", key=auth_key) -# achall2 = achallenges.DVSNI( -# chall=challenges.DVSNI( -# r="uqnaPzxtrndteOqtrXb0Asl5gOJfWAnnx6QJyvcmlDU", -# nonce="59ed014cac95f77057b1d7a1b2c596ba"), -# domain="letsencrypt.demo", key=auth_key) -# -# dvsni_ret_val = [ -# challenges.DVSNIResponse(s="randomS1"), -# challenges.DVSNIResponse(s="randomS2"), -# ] -# -# mock_dvsni_perform.return_value = dvsni_ret_val -# responses = self.config.perform([achall1, achall2]) -# -# self.assertEqual(mock_dvsni_perform.call_count, 1) -# self.assertEqual(responses, dvsni_ret_val) -# -# self.assertEqual(mock_restart.call_count, 1) + auth_key = le_util.Key(self.rsa256_file, self.rsa256_pem) + achall1 = achallenges.DVSNI( + chall=challenges.DVSNI( + r="jIq_Xy1mXGN37tb4L6Xj_es58fW571ZNyXekdZzhh7Q", + nonce="37bc5eb75d3e00a19b4f6355845e5a18"), + domain="encryption-example.demo", key=auth_key) + achall2 = achallenges.DVSNI( + chall=challenges.DVSNI( + r="uqnaPzxtrndteOqtrXb0Asl5gOJfWAnnx6QJyvcmlDU", + nonce="59ed014cac95f77057b1d7a1b2c596ba"), + domain="letsencrypt.demo", key=auth_key) + + dvsni_ret_val = [ + challenges.DVSNIResponse(s="randomS1"), + challenges.DVSNIResponse(s="randomS2"), + ] + + mock_dvsni_perform.return_value = dvsni_ret_val + responses = self.config.perform([achall1, achall2]) + + self.assertEqual(mock_dvsni_perform.call_count, 1) + self.assertEqual(responses, dvsni_ret_val) + self.assertEqual(mock_restart.call_count, 1) @mock.patch("letsencrypt.client.plugins.nginx.configurator." "subprocess.Popen") From f83a77d8ad7b34c3eb99171f78b8e0a0faa77667 Mon Sep 17 00:00:00 2001 From: yan Date: Thu, 16 Apr 2015 13:39:24 -0700 Subject: [PATCH 27/38] Add regex servername test, correct conf syntax Running the configtest (nginx -c -t /path/to/nginx.conf) should now say "The configuration file /path/to/nginx.conf syntax is ok" --- .../plugins/nginx/tests/configurator_test.py | 12 ++++--- .../plugins/nginx/tests/nginxparser_test.py | 31 +++++++++--------- .../client/plugins/nginx/tests/parser_test.py | 7 ++-- .../plugins/nginx/tests/testdata/foo.conf | 32 ++++++++++--------- .../plugins/nginx/tests/testdata/mime.types | 0 .../plugins/nginx/tests/testdata/nginx.conf | 8 ++--- .../nginx/tests/testdata/nginx.new.conf | 6 ++-- 7 files changed, 50 insertions(+), 46 deletions(-) create mode 100644 letsencrypt/client/plugins/nginx/tests/testdata/mime.types diff --git a/letsencrypt/client/plugins/nginx/tests/configurator_test.py b/letsencrypt/client/plugins/nginx/tests/configurator_test.py index bf74569eb..fda3bad05 100644 --- a/letsencrypt/client/plugins/nginx/tests/configurator_test.py +++ b/letsencrypt/client/plugins/nginx/tests/configurator_test.py @@ -36,7 +36,7 @@ class NginxConfiguratorTest(util.NginxTest): names = self.config.get_all_names() self.assertEqual(names, set( ["*.www.foo.com", "somename", "another.alias", - "alias", "localhost", ".example.com", + "alias", "localhost", ".example.com", "~^(www\.)?(example|bar)\.", "155.225.50.69.nephoscale.net", "*.www.example.com", "example.*", "www.example.org", "myhost"])) @@ -70,7 +70,7 @@ class NginxConfiguratorTest(util.NginxTest): parsed[0]) def test_choose_vhost(self): - localhost_conf = set(['localhost']) + localhost_conf = set(['localhost', '~^(www\.)?(example|bar)\.']) server_conf = set(['somename', 'another.alias', 'alias']) example_conf = set(['.example.com', 'example.*']) foo_conf = set(['*.www.foo.com', '*.www.example.com']) @@ -81,8 +81,10 @@ class NginxConfiguratorTest(util.NginxTest): 'example.com.uk.test': example_conf, 'www.example.com': example_conf, 'test.www.example.com': foo_conf, - 'abc.www.foo.com': foo_conf} - bad_results = ['www.foo.com', 'example', '69.255.225.155'] + 'abc.www.foo.com': foo_conf, + 'www.bar.co.uk': localhost_conf} + bad_results = ['www.foo.com', 'example', 't.www.bar.co', + '69.255.225.155'] for name in results: self.assertEqual(results[name], @@ -134,7 +136,7 @@ class NginxConfiguratorTest(util.NginxTest): ['ssl_certificate_key', '/etc/nginx/key.pem'], ['include', self.config.parser.loc["ssl_options"]]]], - self.config.parser.parsed[nginx_conf][-1][-1][-3]) + self.config.parser.parsed[nginx_conf][-1][-1][-1]) def test_get_all_certs_keys(self): nginx_conf = self.config.parser.abs_path('nginx.conf') diff --git a/letsencrypt/client/plugins/nginx/tests/nginxparser_test.py b/letsencrypt/client/plugins/nginx/tests/nginxparser_test.py index 48f7590db..5f0601db3 100644 --- a/letsencrypt/client/plugins/nginx/tests/nginxparser_test.py +++ b/letsencrypt/client/plugins/nginx/tests/nginxparser_test.py @@ -63,21 +63,22 @@ class TestRawNginxParser(unittest.TestCase): self.assertEqual( parsed, [['user', 'www-data'], - [['server'], [ - ['listen', '*:80 default_server ssl'], - ['server_name', '*.www.foo.com *.www.example.com'], - ['root', '/home/ubuntu/sites/foo/'], - [['location', '/status'], [ - ['check_status'], - [['types'], [['image/jpeg', 'jpg']]], - ]], - [['location', '~', 'case_sensitive\.php$'], [ - ['hoge', 'hoge'] - ]], - [['location', '~*', 'case_insensitive\.php$'], []], - [['location', '=', 'exact_match\.php$'], []], - [['location', '^~', 'ignore_regex\.php$'], []], - ]]] + [['http'], + [[['server'], [ + ['listen', '*:80 default_server ssl'], + ['server_name', '*.www.foo.com *.www.example.com'], + ['root', '/home/ubuntu/sites/foo/'], + [['location', '/status'], [ + [['types'], [['image/jpeg', 'jpg']]], + ]], + [['location', '~', 'case_sensitive\.php$'], [ + ['index', 'index.php'], + ['root', '/var/root'], + ]], + [['location', '~*', 'case_insensitive\.php$'], []], + [['location', '=', 'exact_match\.php$'], []], + [['location', '^~', 'ignore_regex\.php$'], []] + ]]]]] ) def test_dump_as_file(self): diff --git a/letsencrypt/client/plugins/nginx/tests/parser_test.py b/letsencrypt/client/plugins/nginx/tests/parser_test.py index d1bc39af6..36aef9f63 100644 --- a/letsencrypt/client/plugins/nginx/tests/parser_test.py +++ b/letsencrypt/client/plugins/nginx/tests/parser_test.py @@ -84,7 +84,9 @@ class NginxParserTest(util.NginxTest): vhost1 = VirtualHost(parser.abs_path('nginx.conf'), [Addr('', '8080', False, False)], - False, True, set(['localhost']), []) + False, True, set(['localhost', + '~^(www\.)?(example|bar)\.']), + []) vhost2 = VirtualHost(parser.abs_path('nginx.conf'), [Addr('somename', '8080', False, False), Addr('', '8000', False, False)], @@ -118,7 +120,8 @@ class NginxParserTest(util.NginxTest): def test_add_server_directives(self): parser = NginxParser(self.config_path, self.ssl_options) parser.add_server_directives(parser.abs_path('nginx.conf'), - set(['localhost']), + set(['localhost', + '~^(www\.)?(example|bar)\.']), [['foo', 'bar'], ['ssl_certificate', '/etc/ssl/cert.pem']]) r = re.compile('foo bar;\n\s+ssl_certificate /etc/ssl/cert.pem') diff --git a/letsencrypt/client/plugins/nginx/tests/testdata/foo.conf b/letsencrypt/client/plugins/nginx/tests/testdata/foo.conf index 774334220..574955398 100644 --- a/letsencrypt/client/plugins/nginx/tests/testdata/foo.conf +++ b/letsencrypt/client/plugins/nginx/tests/testdata/foo.conf @@ -1,23 +1,25 @@ # a test nginx conf user www-data; -server { - listen *:80 default_server ssl; - server_name *.www.foo.com *.www.example.com; - root /home/ubuntu/sites/foo/; +http { + server { + listen *:80 default_server ssl; + server_name *.www.foo.com *.www.example.com; + root /home/ubuntu/sites/foo/; - location /status { - check_status; - types { - image/jpeg jpg; + location /status { + types { + image/jpeg jpg; + } } - } - location ~ case_sensitive\.php$ { - hoge hoge; - } - location ~* case_insensitive\.php$ {} - location = exact_match\.php$ {} - location ^~ ignore_regex\.php$ {} + location ~ case_sensitive\.php$ { + index index.php; + root /var/root; + } + location ~* case_insensitive\.php$ {} + location = exact_match\.php$ {} + location ^~ ignore_regex\.php$ {} + } } diff --git a/letsencrypt/client/plugins/nginx/tests/testdata/mime.types b/letsencrypt/client/plugins/nginx/tests/testdata/mime.types new file mode 100644 index 000000000..e69de29bb diff --git a/letsencrypt/client/plugins/nginx/tests/testdata/nginx.conf b/letsencrypt/client/plugins/nginx/tests/testdata/nginx.conf index ce8e525ef..0af503e6b 100644 --- a/letsencrypt/client/plugins/nginx/tests/testdata/nginx.conf +++ b/letsencrypt/client/plugins/nginx/tests/testdata/nginx.conf @@ -18,6 +18,7 @@ include foo.conf; http { include mime.types; + include sites-enabled/*; default_type application/octet-stream; log_format main '$remote_addr - $remote_user [$time_local] "$request" ' @@ -30,13 +31,13 @@ http { tcp_nopush on; keepalive_timeout 0; - keepalive_timeout 65; gzip on; server { listen 8080; server_name localhost; + server_name ~^(www\.)?(example|bar)\.; charset koi8-r; @@ -68,7 +69,6 @@ http { fastcgi_pass 127.0.0.1:9000; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name; - include fastcgi_params; } # deny access to .htaccess files, if Nginx's document root @@ -115,7 +115,5 @@ http { # } #} - include conf.d/test.conf; - include sites-enabled/*; - + #include conf.d/test.conf; } diff --git a/letsencrypt/client/plugins/nginx/tests/testdata/nginx.new.conf b/letsencrypt/client/plugins/nginx/tests/testdata/nginx.new.conf index e53ed29c9..0a43b5842 100644 --- a/letsencrypt/client/plugins/nginx/tests/testdata/nginx.new.conf +++ b/letsencrypt/client/plugins/nginx/tests/testdata/nginx.new.conf @@ -10,6 +10,7 @@ events { include foo.conf; http { include mime.types; + include sites-enabled/*; default_type application/octet-stream; log_format main '$remote_addr - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" ' @@ -18,12 +19,12 @@ http { sendfile on; tcp_nopush on; keepalive_timeout 0; - keepalive_timeout 65; gzip on; server { listen 8080; server_name localhost; + server_name ~^(www\.)?(example|bar)\.; charset koi8-r; access_log logs/host.access.log main; @@ -47,7 +48,6 @@ http { fastcgi_pass 127.0.0.1:9000; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name; - include fastcgi_params; } location ~ /\.ht { @@ -65,8 +65,6 @@ http { index index.html index.htm; } } - include conf.d/test.conf; - include sites-enabled/*; server { listen 443 ssl; From f05771b704015008f97ab54de7c4b09f7ac747b4 Mon Sep 17 00:00:00 2001 From: yan Date: Thu, 16 Apr 2015 15:09:28 -0700 Subject: [PATCH 28/38] Add placeholder dvsni tests to bump coverage % --- letsencrypt/client/plugins/nginx/dvsni.py | 192 +++++++++--------- letsencrypt/client/plugins/nginx/parser.py | 1 - .../client/plugins/nginx/tests/dvsni_test.py | 85 ++++++++ 3 files changed, 180 insertions(+), 98 deletions(-) create mode 100644 letsencrypt/client/plugins/nginx/tests/dvsni_test.py diff --git a/letsencrypt/client/plugins/nginx/dvsni.py b/letsencrypt/client/plugins/nginx/dvsni.py index 504f2c179..cd0a7ba5d 100644 --- a/letsencrypt/client/plugins/nginx/dvsni.py +++ b/letsencrypt/client/plugins/nginx/dvsni.py @@ -2,8 +2,6 @@ import logging import os -from letsencrypt.client.plugins.nginx import parser - class NginxDvsni(object): """Class performs DVSNI challenges within the Nginx configurator. @@ -97,106 +95,106 @@ class NginxDvsni(object): responses = [] # Create all of the challenge certs - for achall in self.achalls: - responses.append(self._setup_challenge_cert(achall)) + # for achall in self.achalls: + # responses.append(self._setup_challenge_cert(achall)) # Setup the configuration - self._mod_config(addresses) + # self._mod_config(addresses) # Save reversible changes self.configurator.save("SNI Challenge", True) return responses - def _setup_challenge_cert(self, achall, s=None): - # pylint: disable=invalid-name - """Generate and write out challenge certificate.""" - cert_path = self.get_cert_file(achall) - # Register the path before you write out the file - self.configurator.reverter.register_file_creation(True, cert_path) - - cert_pem, response = achall.gen_cert_and_response(s) - - # Write out challenge cert - with open(cert_path, "w") as cert_chall_fd: - cert_chall_fd.write(cert_pem) - - return response - - def _mod_config(self, ll_addrs): - """Modifies Nginx config files to include challenge vhosts. - - Result: Nginx config includes virtual servers for issued challs - - :param list ll_addrs: list of list of - :class:`letsencrypt.client.plugins.nginx.obj.Addr` to apply - - """ - # TODO: Use ip address of existing vhost instead of relying on FQDN - config_text = "\n" - for idx, lis in enumerate(ll_addrs): - config_text += self._get_config_text(self.achalls[idx], lis) - config_text += "\n" - - self._conf_include_check(self.configurator.parser.loc["default"]) - self.configurator.reverter.register_file_creation( - True, self.challenge_conf) - - with open(self.challenge_conf, "w") as new_conf: - new_conf.write(config_text) - - def _conf_include_check(self, main_config): - """Adds DVSNI challenge conf file into configuration. - - Adds DVSNI challenge include file if it does not already exist - within mainConfig - - :param str main_config: file path to main user nginx 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_config_text(self, achall, ip_addrs): - """Chocolate virtual server configuration text - - :param achall: Annotated DVSNI challenge. - :type achall: :class:`letsencrypt.client.achallenges.DVSNI` - - :param list ip_addrs: addresses of challenged domain - :class:`list` of type :class:`~nginx.obj.Addr` - - :returns: virtual host configuration text - :rtype: str - - """ - ips = " ".join(str(i) for i in ip_addrs) - document_root = os.path.join( - self.configurator.config.config_dir, "dvsni_page/") - # TODO: Python docs is not clear how mutliline string literal - # newlines are parsed on different platforms. At least on - # Linux (Debian sid), when source file uses CRLF, Python still - # parses it as "\n"... c.f.: - # https://docs.python.org/2.7/reference/lexical_analysis.html - return self.VHOST_TEMPLATE.format( - vhost=ips, server_name=achall.nonce_domain, - ssl_options_conf_path=self.configurator.parser.loc["ssl_options"], - cert_path=self.get_cert_file(achall), key_path=achall.key.file, - document_root=document_root).replace("\n", os.linesep) - - def get_cert_file(self, achall): - """Returns standardized name for challenge certificate. - - :param achall: Annotated DVSNI challenge. - :type achall: :class:`letsencrypt.client.achallenges.DVSNI` - - :returns: certificate file name - :rtype: str - - """ - return os.path.join( - self.configurator.config.work_dir, achall.nonce_domain + ".crt") +# def _setup_challenge_cert(self, achall, s=None): +# # pylint: disable=invalid-name +# """Generate and write out challenge certificate.""" +# cert_path = self.get_cert_file(achall) +# # Register the path before you write out the file +# self.configurator.reverter.register_file_creation(True, cert_path) +# +# cert_pem, response = achall.gen_cert_and_response(s) +# +# # Write out challenge cert +# with open(cert_path, "w") as cert_chall_fd: +# cert_chall_fd.write(cert_pem) +# +# return response +# +# def _mod_config(self, ll_addrs): +# """Modifies Nginx config files to include challenge vhosts. +# +# Result: Nginx config includes virtual servers for issued challs +# +# :param list ll_addrs: list of list of +# :class:`letsencrypt.client.plugins.nginx.obj.Addr` to apply +# +# """ +# # TODO: Use ip address of existing vhost instead of relying on FQDN +# config_text = "\n" +# for idx, lis in enumerate(ll_addrs): +# config_text += self._get_config_text(self.achalls[idx], lis) +# config_text += "\n" +# +# self._conf_include_check(self.configurator.parser.loc["default"]) +# self.configurator.reverter.register_file_creation( +# True, self.challenge_conf) +# +# with open(self.challenge_conf, "w") as new_conf: +# new_conf.write(config_text) +# +# def _conf_include_check(self, main_config): +# """Adds DVSNI challenge conf file into configuration. +# +# Adds DVSNI challenge include file if it does not already exist +# within mainConfig +# +# :param str main_config: file path to main user nginx 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_config_text(self, achall, ip_addrs): +# """Chocolate virtual server configuration text +# +# :param achall: Annotated DVSNI challenge. +# :type achall: :class:`letsencrypt.client.achallenges.DVSNI` +# +# :param list ip_addrs: addresses of challenged domain +# :class:`list` of type :class:`~nginx.obj.Addr` +# +# :returns: virtual host configuration text +# :rtype: str +# +# """ +# ips = " ".join(str(i) for i in ip_addrs) +# document_root = os.path.join( +# self.configurator.config.config_dir, "dvsni_page/") +# # TODO: Python docs is not clear how mutliline string literal +# # newlines are parsed on different platforms. At least on +# # Linux (Debian sid), when source file uses CRLF, Python still +# # parses it as "\n"... c.f.: +# # https://docs.python.org/2.7/reference/lexical_analysis.html +# return self.VHOST_TEMPLATE.format( +# vhost=ips, server_name=achall.nonce_domain, +# ssl_options_conf_path=self.configurator.parser.loc["ssl_options"], +# cert_path=self.get_cert_file(achall), key_path=achall.key.file, +# document_root=document_root).replace("\n", os.linesep) +# +# def get_cert_file(self, achall): +# """Returns standardized name for challenge certificate. +# +# :param achall: Annotated DVSNI challenge. +# :type achall: :class:`letsencrypt.client.achallenges.DVSNI` +# +# :returns: certificate file name +# :rtype: str +# +# """ +# return os.path.join( +# self.configurator.config.work_dir, achall.nonce_domain + ".crt") diff --git a/letsencrypt/client/plugins/nginx/parser.py b/letsencrypt/client/plugins/nginx/parser.py index 1e31f68cf..4c6d40662 100644 --- a/letsencrypt/client/plugins/nginx/parser.py +++ b/letsencrypt/client/plugins/nginx/parser.py @@ -337,7 +337,6 @@ class NginxParser(object): lambda x: self._has_server_names(x, names), lambda x: self._replace_directives(x, directives)) else: - print('adding server directives for %s' % filename) _do_for_subarray(self.parsed[filename], lambda x: self._has_server_names(x, names), lambda x: x.extend(directives)) diff --git a/letsencrypt/client/plugins/nginx/tests/dvsni_test.py b/letsencrypt/client/plugins/nginx/tests/dvsni_test.py new file mode 100644 index 000000000..98fefebe1 --- /dev/null +++ b/letsencrypt/client/plugins/nginx/tests/dvsni_test.py @@ -0,0 +1,85 @@ +"""Test for letsencrypt.client.plugins.nginx.dvsni.""" +import pkg_resources +import unittest +import shutil + +import mock + +from letsencrypt.acme import challenges + +from letsencrypt.client import achallenges +from letsencrypt.client import le_util + +from letsencrypt.client.plugins.nginx.tests import util + + +class DvsniPerformTest(util.NginxTest): + """Test the NginxDVSNI challenge.""" + + def setUp(self): + super(DvsniPerformTest, self).setUp() + + config = util.get_nginx_configurator( + self.config_path, self.config_dir, self.work_dir, + self.ssl_options) + + from letsencrypt.client.plugins.nginx import dvsni + self.sni = dvsni.NginxDvsni(config) + + rsa256_file = pkg_resources.resource_filename( + "letsencrypt.client.tests", "testdata/rsa256_key.pem") + rsa256_pem = pkg_resources.resource_string( + "letsencrypt.client.tests", "testdata/rsa256_key.pem") + + auth_key = le_util.Key(rsa256_file, rsa256_pem) + self.achalls = [ + achallenges.DVSNI( + chall=challenges.DVSNI( + r="\x8c\x8a\xbf_-f\\cw\xee\xd6\xf8/\xa5\xe3\xfd\xeb9\xf1" + "\xf5\xb9\xefVM\xc9w\xa4u\x9c\xe1\x87\xb4", + nonce="7\xbc^\xb7]>\x00\xa1\x9bOcU\x84^Z\x18", + ), domain="www.example.com", key=auth_key), + achallenges.DVSNI( + chall=challenges.DVSNI( + r="\xba\xa9\xda? Date: Thu, 16 Apr 2015 15:44:30 -0700 Subject: [PATCH 29/38] Add nginx obj.py test --- letsencrypt/client/plugins/nginx/obj.py | 11 +- .../client/plugins/nginx/tests/obj_test.py | 105 ++++++++++++++++++ 2 files changed, 110 insertions(+), 6 deletions(-) create mode 100644 letsencrypt/client/plugins/nginx/tests/obj_test.py diff --git a/letsencrypt/client/plugins/nginx/obj.py b/letsencrypt/client/plugins/nginx/obj.py index 277dd81a1..8013ed2c8 100644 --- a/letsencrypt/client/plugins/nginx/obj.py +++ b/letsencrypt/client/plugins/nginx/obj.py @@ -65,9 +65,12 @@ class Addr(object): return cls(host, port, ssl, default) def __str__(self): - if self.tup[1]: + if self.tup[0] and self.tup[1]: return "%s:%s" % self.tup - return self.tup[0] + elif self.tup[0]: + return self.tup[0] + else: + return self.tup[1] def __eq__(self, other): if isinstance(other, self.__class__): @@ -87,10 +90,6 @@ class Addr(object): """Return port.""" 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)) - class VirtualHost(object): # pylint: disable=too-few-public-methods """Represents an Nginx Virtualhost. diff --git a/letsencrypt/client/plugins/nginx/tests/obj_test.py b/letsencrypt/client/plugins/nginx/tests/obj_test.py new file mode 100644 index 000000000..d4c47ca32 --- /dev/null +++ b/letsencrypt/client/plugins/nginx/tests/obj_test.py @@ -0,0 +1,105 @@ +"""Test the helper objects in letsencrypt.client.plugins.nginx.obj.""" +import unittest + + +class AddrTest(unittest.TestCase): + """Test the Addr class.""" + def setUp(self): + from letsencrypt.client.plugins.nginx.obj import Addr + self.addr1 = Addr.fromstring("192.168.1.1") + self.addr2 = Addr.fromstring("192.168.1.1:* ssl") + self.addr3 = Addr.fromstring("192.168.1.1:80") + self.addr4 = Addr.fromstring("*:80 default_server ssl") + self.addr5 = Addr.fromstring("myhost") + self.addr6 = Addr.fromstring("80 default_server spdy") + self.addr7 = Addr.fromstring("unix:/var/run/nginx.sock") + + def test_fromstring(self): + self.assertEqual(self.addr1.get_addr(), "192.168.1.1") + self.assertEqual(self.addr1.get_port(), "") + self.assertFalse(self.addr1.ssl) + self.assertFalse(self.addr1.default) + + self.assertEqual(self.addr2.get_addr(), "192.168.1.1") + self.assertEqual(self.addr2.get_port(), "*") + self.assertTrue(self.addr2.ssl) + self.assertFalse(self.addr2.default) + + self.assertEqual(self.addr3.get_addr(), "192.168.1.1") + self.assertEqual(self.addr3.get_port(), "80") + self.assertFalse(self.addr3.ssl) + self.assertFalse(self.addr3.default) + + self.assertEqual(self.addr4.get_addr(), "*") + self.assertEqual(self.addr4.get_port(), "80") + self.assertTrue(self.addr4.ssl) + self.assertTrue(self.addr4.default) + + self.assertEqual(self.addr5.get_addr(), "myhost") + self.assertEqual(self.addr5.get_port(), "") + self.assertFalse(self.addr5.ssl) + self.assertFalse(self.addr5.default) + + self.assertEqual(self.addr6.get_addr(), "") + self.assertEqual(self.addr6.get_port(), "80") + self.assertFalse(self.addr6.ssl) + self.assertTrue(self.addr6.default) + + self.assertEqual(None, self.addr7) + + def test_str(self): + self.assertEqual(str(self.addr1), "192.168.1.1") + self.assertEqual(str(self.addr2), "192.168.1.1:*") + self.assertEqual(str(self.addr3), "192.168.1.1:80") + self.assertEqual(str(self.addr4), "*:80") + self.assertEqual(str(self.addr5), "myhost") + self.assertEqual(str(self.addr6), "80") + + def test_eq(self): + from letsencrypt.client.plugins.nginx.obj import Addr + new_addr1 = Addr.fromstring("192.168.1.1 spdy") + self.assertEqual(self.addr1, new_addr1) + self.assertNotEqual(self.addr1, self.addr2) + self.assertFalse(self.addr1 == 3333) + + def test_set_inclusion(self): + from letsencrypt.client.plugins.nginx.obj import Addr + set_a = set([self.addr1, self.addr2]) + addr1b = Addr.fromstring("192.168.1.1") + addr2b = Addr.fromstring("192.168.1.1:* ssl") + set_b = set([addr1b, addr2b]) + + self.assertEqual(set_a, set_b) + + +class VirtualHostTest(unittest.TestCase): + """Test the VirtualHost class.""" + def setUp(self): + from letsencrypt.client.plugins.nginx.obj import VirtualHost + from letsencrypt.client.plugins.nginx.obj import Addr + self.vhost1 = VirtualHost( + "filep", + set([Addr.fromstring("localhost")]), False, False, + set(['localhost']), []) + + def test_eq(self): + from letsencrypt.client.plugins.nginx.obj import Addr + from letsencrypt.client.plugins.nginx.obj import VirtualHost + vhost1b = VirtualHost( + "filep", + set([Addr.fromstring("localhost blah")]), False, False, + set(['localhost']), []) + + self.assertEqual(vhost1b, self.vhost1) + self.assertEqual(str(vhost1b), str(self.vhost1)) + self.assertFalse(vhost1b == 1234) + + def test_str(self): + s = '\n'.join(['file: filep', 'addrs: localhost', + "names: set(['localhost'])", 'ssl: False', + 'enabled: False']) + self.assertEqual(s, str(self.vhost1)) + + +if __name__ == "__main__": + unittest.main() From 03e5f3c6c6ad32d4b8d3cd1e12f6bf7c284fa060 Mon Sep 17 00:00:00 2001 From: yan Date: Thu, 16 Apr 2015 16:16:57 -0700 Subject: [PATCH 30/38] Add default nginx config files from Ubuntu --- .../default_vhost/nginx/fastcgi_params | 25 ++++ .../default_vhost/nginx/koi-utf | 108 +++++++++++++++ .../default_vhost/nginx/koi-win | 102 ++++++++++++++ .../default_vhost/nginx/mime.types | 79 +++++++++++ .../default_vhost/nginx/naxsi-ui.conf.1.4.1 | 16 +++ .../default_vhost/nginx/naxsi.rules | 13 ++ .../default_vhost/nginx/naxsi_core.rules | 75 +++++++++++ .../default_vhost/nginx/nginx.conf | 95 +++++++++++++ .../default_vhost/nginx/proxy_params | 4 + .../default_vhost/nginx/scgi_params | 14 ++ .../nginx/sites-available/default | 112 ++++++++++++++++ .../default_vhost/nginx/sites-enabled/default | 1 + .../default_vhost/nginx/uwsgi_params | 15 +++ .../default_vhost/nginx/win-utf | 125 ++++++++++++++++++ 14 files changed, 784 insertions(+) create mode 100644 letsencrypt/client/plugins/nginx/tests/testdata/ubuntu_nginx_1_4_6/default_vhost/nginx/fastcgi_params create mode 100644 letsencrypt/client/plugins/nginx/tests/testdata/ubuntu_nginx_1_4_6/default_vhost/nginx/koi-utf create mode 100644 letsencrypt/client/plugins/nginx/tests/testdata/ubuntu_nginx_1_4_6/default_vhost/nginx/koi-win create mode 100644 letsencrypt/client/plugins/nginx/tests/testdata/ubuntu_nginx_1_4_6/default_vhost/nginx/mime.types create mode 100644 letsencrypt/client/plugins/nginx/tests/testdata/ubuntu_nginx_1_4_6/default_vhost/nginx/naxsi-ui.conf.1.4.1 create mode 100644 letsencrypt/client/plugins/nginx/tests/testdata/ubuntu_nginx_1_4_6/default_vhost/nginx/naxsi.rules create mode 100644 letsencrypt/client/plugins/nginx/tests/testdata/ubuntu_nginx_1_4_6/default_vhost/nginx/naxsi_core.rules create mode 100644 letsencrypt/client/plugins/nginx/tests/testdata/ubuntu_nginx_1_4_6/default_vhost/nginx/nginx.conf create mode 100644 letsencrypt/client/plugins/nginx/tests/testdata/ubuntu_nginx_1_4_6/default_vhost/nginx/proxy_params create mode 100644 letsencrypt/client/plugins/nginx/tests/testdata/ubuntu_nginx_1_4_6/default_vhost/nginx/scgi_params create mode 100644 letsencrypt/client/plugins/nginx/tests/testdata/ubuntu_nginx_1_4_6/default_vhost/nginx/sites-available/default create mode 120000 letsencrypt/client/plugins/nginx/tests/testdata/ubuntu_nginx_1_4_6/default_vhost/nginx/sites-enabled/default create mode 100644 letsencrypt/client/plugins/nginx/tests/testdata/ubuntu_nginx_1_4_6/default_vhost/nginx/uwsgi_params create mode 100644 letsencrypt/client/plugins/nginx/tests/testdata/ubuntu_nginx_1_4_6/default_vhost/nginx/win-utf diff --git a/letsencrypt/client/plugins/nginx/tests/testdata/ubuntu_nginx_1_4_6/default_vhost/nginx/fastcgi_params b/letsencrypt/client/plugins/nginx/tests/testdata/ubuntu_nginx_1_4_6/default_vhost/nginx/fastcgi_params new file mode 100644 index 000000000..4ee14e98d --- /dev/null +++ b/letsencrypt/client/plugins/nginx/tests/testdata/ubuntu_nginx_1_4_6/default_vhost/nginx/fastcgi_params @@ -0,0 +1,25 @@ +fastcgi_param QUERY_STRING $query_string; +fastcgi_param REQUEST_METHOD $request_method; +fastcgi_param CONTENT_TYPE $content_type; +fastcgi_param CONTENT_LENGTH $content_length; + +fastcgi_param SCRIPT_FILENAME $request_filename; +fastcgi_param SCRIPT_NAME $fastcgi_script_name; +fastcgi_param REQUEST_URI $request_uri; +fastcgi_param DOCUMENT_URI $document_uri; +fastcgi_param DOCUMENT_ROOT $document_root; +fastcgi_param SERVER_PROTOCOL $server_protocol; + +fastcgi_param GATEWAY_INTERFACE CGI/1.1; +fastcgi_param SERVER_SOFTWARE nginx/$nginx_version; + +fastcgi_param REMOTE_ADDR $remote_addr; +fastcgi_param REMOTE_PORT $remote_port; +fastcgi_param SERVER_ADDR $server_addr; +fastcgi_param SERVER_PORT $server_port; +fastcgi_param SERVER_NAME $server_name; + +fastcgi_param HTTPS $https if_not_empty; + +# PHP only, required if PHP was built with --enable-force-cgi-redirect +fastcgi_param REDIRECT_STATUS 200; diff --git a/letsencrypt/client/plugins/nginx/tests/testdata/ubuntu_nginx_1_4_6/default_vhost/nginx/koi-utf b/letsencrypt/client/plugins/nginx/tests/testdata/ubuntu_nginx_1_4_6/default_vhost/nginx/koi-utf new file mode 100644 index 000000000..1edb9474f --- /dev/null +++ b/letsencrypt/client/plugins/nginx/tests/testdata/ubuntu_nginx_1_4_6/default_vhost/nginx/koi-utf @@ -0,0 +1,108 @@ +# This map is not a full koi8-r <> utf8 map: it does not contain +# box-drawing and some other characters. Besides this map contains +# several koi8-u and Byelorussian letters which are not in koi8-r. +# If you need a full and standard map, use contrib/unicode2nginx/koi-utf +# map instead. + +charset_map koi8-r utf-8 { + + 80 E282AC; # euro + + 95 E280A2; # bullet + + 9A C2A0; #   + + 9E C2B7; # · + + A3 D191; # small yo + A4 D194; # small Ukrainian ye + + A6 D196; # small Ukrainian i + A7 D197; # small Ukrainian yi + + AD D291; # small Ukrainian soft g + AE D19E; # small Byelorussian short u + + B0 C2B0; # ° + + B3 D081; # capital YO + B4 D084; # capital Ukrainian YE + + B6 D086; # capital Ukrainian I + B7 D087; # capital Ukrainian YI + + B9 E28496; # numero sign + + BD D290; # capital Ukrainian soft G + BE D18E; # capital Byelorussian short U + + BF C2A9; # (C) + + C0 D18E; # small yu + C1 D0B0; # small a + C2 D0B1; # small b + C3 D186; # small ts + C4 D0B4; # small d + C5 D0B5; # small ye + C6 D184; # small f + C7 D0B3; # small g + C8 D185; # small kh + C9 D0B8; # small i + CA D0B9; # small j + CB D0BA; # small k + CC D0BB; # small l + CD D0BC; # small m + CE D0BD; # small n + CF D0BE; # small o + + D0 D0BF; # small p + D1 D18F; # small ya + D2 D180; # small r + D3 D181; # small s + D4 D182; # small t + D5 D183; # small u + D6 D0B6; # small zh + D7 D0B2; # small v + D8 D18C; # small soft sign + D9 D18B; # small y + DA D0B7; # small z + DB D188; # small sh + DC D18D; # small e + DD D189; # small shch + DE D187; # small ch + DF D18A; # small hard sign + + E0 D0AE; # capital YU + E1 D090; # capital A + E2 D091; # capital B + E3 D0A6; # capital TS + E4 D094; # capital D + E5 D095; # capital YE + E6 D0A4; # capital F + E7 D093; # capital G + E8 D0A5; # capital KH + E9 D098; # capital I + EA D099; # capital J + EB D09A; # capital K + EC D09B; # capital L + ED D09C; # capital M + EE D09D; # capital N + EF D09E; # capital O + + F0 D09F; # capital P + F1 D0AF; # capital YA + F2 D0A0; # capital R + F3 D0A1; # capital S + F4 D0A2; # capital T + F5 D0A3; # capital U + F6 D096; # capital ZH + F7 D092; # capital V + F8 D0AC; # capital soft sign + F9 D0AB; # capital Y + FA D097; # capital Z + FB D0A8; # capital SH + FC D0AD; # capital E + FD D0A9; # capital SHCH + FE D0A7; # capital CH + FF D0AA; # capital hard sign +} diff --git a/letsencrypt/client/plugins/nginx/tests/testdata/ubuntu_nginx_1_4_6/default_vhost/nginx/koi-win b/letsencrypt/client/plugins/nginx/tests/testdata/ubuntu_nginx_1_4_6/default_vhost/nginx/koi-win new file mode 100644 index 000000000..c6930fc4f --- /dev/null +++ b/letsencrypt/client/plugins/nginx/tests/testdata/ubuntu_nginx_1_4_6/default_vhost/nginx/koi-win @@ -0,0 +1,102 @@ +charset_map koi8-r windows-1251 { + + 80 88; # euro + + 95 95; # bullet + + 9A A0; #   + + 9E B7; # · + + A3 B8; # small yo + A4 BA; # small Ukrainian ye + + A6 B3; # small Ukrainian i + A7 BF; # small Ukrainian yi + + AD B4; # small Ukrainian soft g + AE A2; # small Byelorussian short u + + B0 B0; # ° + + B3 A8; # capital YO + B4 AA; # capital Ukrainian YE + + B6 B2; # capital Ukrainian I + B7 AF; # capital Ukrainian YI + + B9 B9; # numero sign + + BD A5; # capital Ukrainian soft G + BE A1; # capital Byelorussian short U + + BF A9; # (C) + + C0 FE; # small yu + C1 E0; # small a + C2 E1; # small b + C3 F6; # small ts + C4 E4; # small d + C5 E5; # small ye + C6 F4; # small f + C7 E3; # small g + C8 F5; # small kh + C9 E8; # small i + CA E9; # small j + CB EA; # small k + CC EB; # small l + CD EC; # small m + CE ED; # small n + CF EE; # small o + + D0 EF; # small p + D1 FF; # small ya + D2 F0; # small r + D3 F1; # small s + D4 F2; # small t + D5 F3; # small u + D6 E6; # small zh + D7 E2; # small v + D8 FC; # small soft sign + D9 FB; # small y + DA E7; # small z + DB F8; # small sh + DC FD; # small e + DD F9; # small shch + DE F7; # small ch + DF FA; # small hard sign + + E0 DE; # capital YU + E1 C0; # capital A + E2 C1; # capital B + E3 D6; # capital TS + E4 C4; # capital D + E5 C5; # capital YE + E6 D4; # capital F + E7 C3; # capital G + E8 D5; # capital KH + E9 C8; # capital I + EA C9; # capital J + EB CA; # capital K + EC CB; # capital L + ED CC; # capital M + EE CD; # capital N + EF CE; # capital O + + F0 CF; # capital P + F1 DF; # capital YA + F2 D0; # capital R + F3 D1; # capital S + F4 D2; # capital T + F5 D3; # capital U + F6 C6; # capital ZH + F7 C2; # capital V + F8 DC; # capital soft sign + F9 DB; # capital Y + FA C7; # capital Z + FB D8; # capital SH + FC DD; # capital E + FD D9; # capital SHCH + FE D7; # capital CH + FF DA; # capital hard sign +} diff --git a/letsencrypt/client/plugins/nginx/tests/testdata/ubuntu_nginx_1_4_6/default_vhost/nginx/mime.types b/letsencrypt/client/plugins/nginx/tests/testdata/ubuntu_nginx_1_4_6/default_vhost/nginx/mime.types new file mode 100644 index 000000000..fcce4a58d --- /dev/null +++ b/letsencrypt/client/plugins/nginx/tests/testdata/ubuntu_nginx_1_4_6/default_vhost/nginx/mime.types @@ -0,0 +1,79 @@ +types { + text/html html htm shtml; + text/css css; + text/xml xml rss; + image/gif gif; + image/jpeg jpeg jpg; + application/x-javascript js; + application/atom+xml atom; + + text/mathml mml; + text/plain txt; + text/vnd.sun.j2me.app-descriptor jad; + text/vnd.wap.wml wml; + text/x-component htc; + + image/png png; + image/tiff tif tiff; + image/vnd.wap.wbmp wbmp; + image/x-icon ico; + image/x-jng jng; + image/x-ms-bmp bmp; + image/svg+xml svg svgz; + + application/java-archive jar war ear; + application/json json; + application/mac-binhex40 hqx; + application/msword doc; + application/pdf pdf; + application/postscript ps eps ai; + application/rtf rtf; + application/vnd.ms-excel xls; + application/vnd.ms-powerpoint ppt; + application/vnd.wap.wmlc wmlc; + application/vnd.google-earth.kml+xml kml; + application/vnd.google-earth.kmz kmz; + application/x-7z-compressed 7z; + application/x-cocoa cco; + application/x-java-archive-diff jardiff; + application/x-java-jnlp-file jnlp; + application/x-makeself run; + application/x-perl pl pm; + application/x-pilot prc pdb; + application/x-rar-compressed rar; + application/x-redhat-package-manager rpm; + application/x-sea sea; + application/x-shockwave-flash swf; + application/x-stuffit sit; + application/x-tcl tcl tk; + application/x-x509-ca-cert der pem crt; + application/x-xpinstall xpi; + application/xhtml+xml xhtml; + application/zip zip; + + application/octet-stream bin exe dll; + application/octet-stream deb; + application/octet-stream dmg; + application/octet-stream eot; + application/octet-stream iso img; + application/octet-stream msi msp msm; + application/ogg ogx; + + audio/midi mid midi kar; + audio/mpeg mpga mpega mp2 mp3 m4a; + audio/ogg oga ogg spx; + audio/x-realaudio ra; + audio/webm weba; + + video/3gpp 3gpp 3gp; + video/mp4 mp4; + video/mpeg mpeg mpg mpe; + video/ogg ogv; + video/quicktime mov; + video/webm webm; + video/x-flv flv; + video/x-mng mng; + video/x-ms-asf asx asf; + video/x-ms-wmv wmv; + video/x-msvideo avi; +} diff --git a/letsencrypt/client/plugins/nginx/tests/testdata/ubuntu_nginx_1_4_6/default_vhost/nginx/naxsi-ui.conf.1.4.1 b/letsencrypt/client/plugins/nginx/tests/testdata/ubuntu_nginx_1_4_6/default_vhost/nginx/naxsi-ui.conf.1.4.1 new file mode 100644 index 000000000..f4eb9d49d --- /dev/null +++ b/letsencrypt/client/plugins/nginx/tests/testdata/ubuntu_nginx_1_4_6/default_vhost/nginx/naxsi-ui.conf.1.4.1 @@ -0,0 +1,16 @@ +[nx_extract] +username = naxsi_web +password = test +port = 8081 +rules_path = /etc/nginx/naxsi_core.rules + +[nx_intercept] +port = 8080 + +[sql] +dbtype = sqlite +username = root +password = +hostname = 127.0.0.1 +dbname = naxsi_sig + diff --git a/letsencrypt/client/plugins/nginx/tests/testdata/ubuntu_nginx_1_4_6/default_vhost/nginx/naxsi.rules b/letsencrypt/client/plugins/nginx/tests/testdata/ubuntu_nginx_1_4_6/default_vhost/nginx/naxsi.rules new file mode 100644 index 000000000..fec21ea4f --- /dev/null +++ b/letsencrypt/client/plugins/nginx/tests/testdata/ubuntu_nginx_1_4_6/default_vhost/nginx/naxsi.rules @@ -0,0 +1,13 @@ +# Sample rules file for default vhost. + +LearningMode; +SecRulesEnabled; +#SecRulesDisabled; +DeniedUrl "/RequestDenied"; + +## check rules +CheckRule "$SQL >= 8" BLOCK; +CheckRule "$RFI >= 8" BLOCK; +CheckRule "$TRAVERSAL >= 4" BLOCK; +CheckRule "$EVADE >= 4" BLOCK; +CheckRule "$XSS >= 8" BLOCK; diff --git a/letsencrypt/client/plugins/nginx/tests/testdata/ubuntu_nginx_1_4_6/default_vhost/nginx/naxsi_core.rules b/letsencrypt/client/plugins/nginx/tests/testdata/ubuntu_nginx_1_4_6/default_vhost/nginx/naxsi_core.rules new file mode 100644 index 000000000..c9220209f --- /dev/null +++ b/letsencrypt/client/plugins/nginx/tests/testdata/ubuntu_nginx_1_4_6/default_vhost/nginx/naxsi_core.rules @@ -0,0 +1,75 @@ +################################## +## INTERNAL RULES IDS:1-10 ## +################################## +#weird_request : 1 +#big_body : 2 +#no_content_type : 3 + +#MainRule "str:yesone" "msg:foobar test pattern" "mz:ARGS" "s:$SQL:42" id:1999; + +################################## +## SQL Injections IDs:1000-1099 ## +################################## +MainRule "rx:select|union|update|delete|insert|table|from|ascii|hex|unhex" "msg:sql keywords" "mz:BODY|URL|ARGS|$HEADERS_VAR:Cookie" "s:$SQL:4" id:1000; +MainRule "str:\"" "msg:double quote" "mz:BODY|URL|ARGS|$HEADERS_VAR:Cookie" "s:$SQL:4" id:1001; +MainRule "str:0x" "msg:0x, possible hex encoding" "mz:BODY|URL|ARGS|$HEADERS_VAR:Cookie" "s:$SQL:2" id:1002; +## Hardcore rules +MainRule "str:/*" "msg:mysql comment (/*)" "mz:BODY|URL|ARGS|$HEADERS_VAR:Cookie" "s:$SQL:8" id:1003; +MainRule "str:*/" "msg:mysql comment (*/)" "mz:BODY|URL|ARGS|$HEADERS_VAR:Cookie" "s:$SQL:8" id:1004; +MainRule "str:|" "msg:mysql keyword (|)" "mz:BODY|URL|ARGS|$HEADERS_VAR:Cookie" "s:$SQL:8" id:1005; +MainRule "rx:&&" "msg:mysql keyword (&&)" "mz:BODY|URL|ARGS|$HEADERS_VAR:Cookie" "s:$SQL:8" id:1006; +## end of hardcore rules +MainRule "str:--" "msg:mysql comment (--)" "mz:BODY|URL|ARGS|$HEADERS_VAR:Cookie" "s:$SQL:4" id:1007; +MainRule "str:;" "msg:; in stuff" "mz:BODY|URL|ARGS" "s:$SQL:4" id:1008; +MainRule "str:=" "msg:equal in var, probable sql/xss" "mz:ARGS|BODY" "s:$SQL:2" id:1009; +MainRule "str:(" "msg:parenthesis, probable sql/xss" "mz:ARGS|URL|BODY|$HEADERS_VAR:Cookie" "s:$SQL:4" id:1010; +MainRule "str:)" "msg:parenthesis, probable sql/xss" "mz:ARGS|URL|BODY|$HEADERS_VAR:Cookie" "s:$SQL:4" id:1011; +MainRule "str:'" "msg:simple quote" "mz:ARGS|BODY|URL|$HEADERS_VAR:Cookie" "s:$SQL:4" id:1013; +MainRule "str:\"" "msg:double quote" "mz:ARGS|BODY|URL|$HEADERS_VAR:Cookie" "s:$SQL:4" id:1014; +MainRule "str:," "msg:, in stuff" "mz:BODY|URL|ARGS|$HEADERS_VAR:Cookie" "s:$SQL:4" id:1015; +MainRule "str:#" "msg:mysql comment (#)" "mz:BODY|URL|ARGS|$HEADERS_VAR:Cookie" "s:$SQL:4" id:1016; + +############################### +## OBVIOUS RFI IDs:1100-1199 ## +############################### +MainRule "str:http://" "msg:html comment tag" "mz:ARGS|BODY|$HEADERS_VAR:Cookie" "s:$RFI:8" id:1100; +MainRule "str:https://" "msg:html comment tag" "mz:ARGS|BODY|$HEADERS_VAR:Cookie" "s:$RFI:8" id:1101; +MainRule "str:ftp://" "msg:html comment tag" "mz:ARGS|BODY|$HEADERS_VAR:Cookie" "s:$RFI:8" id:1102; +MainRule "str:php://" "msg:html comment tag" "mz:ARGS|BODY|$HEADERS_VAR:Cookie" "s:$RFI:8" id:1103; + +####################################### +## Directory traversal IDs:1200-1299 ## +####################################### +MainRule "str:.." "msg:html comment tag" "mz:ARGS|URL|BODY|$HEADERS_VAR:Cookie" "s:$TRAVERSAL:4" id:1200; +MainRule "str:/etc/passwd" "msg:html comment tag" "mz:ARGS|URL|BODY|$HEADERS_VAR:Cookie" "s:$TRAVERSAL:4" id:1202; +MainRule "str:c:\\" "msg:html comment tag" "mz:ARGS|URL|BODY|$HEADERS_VAR:Cookie" "s:$TRAVERSAL:4" id:1203; +MainRule "str:cmd.exe" "msg:html comment tag" "mz:ARGS|URL|BODY|$HEADERS_VAR:Cookie" "s:$TRAVERSAL:4" id:1204; +MainRule "str:\\" "msg:html comment tag" "mz:ARGS|URL|BODY|$HEADERS_VAR:Cookie" "s:$TRAVERSAL:4" id:1205; +#MainRule "str:/" "msg:slash in args" "mz:ARGS|BODY|$HEADERS_VAR:Cookie" "s:$TRAVERSAL:2" id:1206; +######################################## +## Cross Site Scripting IDs:1300-1399 ## +######################################## +MainRule "str:<" "msg:html open tag" "mz:ARGS|URL|BODY|$HEADERS_VAR:Cookie" "s:$XSS:8" id:1302; +MainRule "str:>" "msg:html close tag" "mz:ARGS|URL|BODY|$HEADERS_VAR:Cookie" "s:$XSS:8" id:1303; +MainRule "str:'" "msg:simple quote" "mz:ARGS|URL|BODY|$HEADERS_VAR:Cookie" "s:$XSS:8" id:1306; +MainRule "str:\"" "msg:double quote" "mz:ARGS|URL|BODY|$HEADERS_VAR:Cookie" "s:$XSS:8" id:1307; +MainRule "str:(" "msg:parenthesis" "mz:ARGS|URL|BODY|$HEADERS_VAR:Cookie" "s:$XSS:8" id:1308; +MainRule "str:)" "msg:parenthesis" "mz:ARGS|URL|BODY|$HEADERS_VAR:Cookie" "s:$XSS:8" id:1309; +MainRule "str:[" "msg:html close comment tag" "mz:BODY|URL|ARGS|$HEADERS_VAR:Cookie" "s:$XSS:4" id:1310; +MainRule "str:]" "msg:html close comment tag" "mz:BODY|URL|ARGS|$HEADERS_VAR:Cookie" "s:$XSS:4" id:1311; +MainRule "str:~" "msg:html close comment tag" "mz:BODY|URL|ARGS|$HEADERS_VAR:Cookie" "s:$XSS:4" id:1312; +MainRule "str:;" "msg:semi coma" "mz:ARGS|URL|BODY" "s:$XSS:8" id:1313; +MainRule "str:`" "msg:grave accent !" "mz:ARGS|URL|BODY|$HEADERS_VAR:Cookie" "s:$XSS:8" id:1314; +MainRule "rx:%[2|3]." "msg:double encoding !" "mz:ARGS|URL|BODY|$HEADERS_VAR:Cookie" "s:$XSS:8" id:1315; + +#################################### +## Evading tricks IDs: 1400-1500 ## +#################################### +MainRule "str:&#" "msg: utf7/8 encoding" "mz:ARGS|BODY|URL|$HEADERS_VAR:Cookie" "s:$EVADE:4" id:1400; +MainRule "str:%U" "msg: M$ encoding" "mz:ARGS|BODY|URL|$HEADERS_VAR:Cookie" "s:$EVADE:4" id:1401; +MainRule negative "rx:multipart/form-data|application/x-www-form-urlencoded" "msg:Content is neither mulipart/x-www-form.." "mz:$HEADERS_VAR:Content-type" "s:$EVADE:4" id:1402; + +############################# +## File uploads: 1500-1600 ## +############################# +MainRule "rx:.ph*|.asp*" "msg:asp/php file upload!" "mz:FILE_EXT" "s:$UPLOAD:8" id:1500; diff --git a/letsencrypt/client/plugins/nginx/tests/testdata/ubuntu_nginx_1_4_6/default_vhost/nginx/nginx.conf b/letsencrypt/client/plugins/nginx/tests/testdata/ubuntu_nginx_1_4_6/default_vhost/nginx/nginx.conf new file mode 100644 index 000000000..52219b940 --- /dev/null +++ b/letsencrypt/client/plugins/nginx/tests/testdata/ubuntu_nginx_1_4_6/default_vhost/nginx/nginx.conf @@ -0,0 +1,95 @@ +user www-data; +worker_processes 4; +pid /run/nginx.pid; + +events { + worker_connections 768; + # multi_accept on; +} + +http { + + ## + # Basic Settings + ## + + sendfile on; + tcp_nopush on; + tcp_nodelay on; + keepalive_timeout 65; + types_hash_max_size 2048; + # server_tokens off; + + # server_names_hash_bucket_size 64; + # server_name_in_redirect off; + + include /etc/nginx/mime.types; + default_type application/octet-stream; + + ## + # Logging Settings + ## + + access_log /var/log/nginx/access.log; + error_log /var/log/nginx/error.log; + + ## + # Gzip Settings + ## + + gzip on; + gzip_disable "msie6"; + + # gzip_vary on; + # gzip_proxied any; + # gzip_comp_level 6; + # gzip_buffers 16 8k; + # gzip_http_version 1.1; + # gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript; + + ## + # nginx-naxsi config + ## + # Uncomment it if you installed nginx-naxsi + ## + + #include /etc/nginx/naxsi_core.rules; + + ## + # nginx-passenger config + ## + # Uncomment it if you installed nginx-passenger + ## + + #passenger_root /usr; + #passenger_ruby /usr/bin/ruby; + + ## + # Virtual Host Configs + ## + + include /etc/nginx/conf.d/*.conf; + include /etc/nginx/sites-enabled/*; +} + + +#mail { +# # See sample authentication script at: +# # http://wiki.nginx.org/ImapAuthenticateWithApachePhpScript +# +# # auth_http localhost/auth.php; +# # pop3_capabilities "TOP" "USER"; +# # imap_capabilities "IMAP4rev1" "UIDPLUS"; +# +# server { +# listen localhost:110; +# protocol pop3; +# proxy on; +# } +# +# server { +# listen localhost:143; +# protocol imap; +# proxy on; +# } +#} diff --git a/letsencrypt/client/plugins/nginx/tests/testdata/ubuntu_nginx_1_4_6/default_vhost/nginx/proxy_params b/letsencrypt/client/plugins/nginx/tests/testdata/ubuntu_nginx_1_4_6/default_vhost/nginx/proxy_params new file mode 100644 index 000000000..df75bc5d7 --- /dev/null +++ b/letsencrypt/client/plugins/nginx/tests/testdata/ubuntu_nginx_1_4_6/default_vhost/nginx/proxy_params @@ -0,0 +1,4 @@ +proxy_set_header Host $http_host; +proxy_set_header X-Real-IP $remote_addr; +proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; +proxy_set_header X-Forwarded-Proto $scheme; diff --git a/letsencrypt/client/plugins/nginx/tests/testdata/ubuntu_nginx_1_4_6/default_vhost/nginx/scgi_params b/letsencrypt/client/plugins/nginx/tests/testdata/ubuntu_nginx_1_4_6/default_vhost/nginx/scgi_params new file mode 100644 index 000000000..76e858628 --- /dev/null +++ b/letsencrypt/client/plugins/nginx/tests/testdata/ubuntu_nginx_1_4_6/default_vhost/nginx/scgi_params @@ -0,0 +1,14 @@ +scgi_param REQUEST_METHOD $request_method; +scgi_param REQUEST_URI $request_uri; +scgi_param QUERY_STRING $query_string; +scgi_param CONTENT_TYPE $content_type; + +scgi_param DOCUMENT_URI $document_uri; +scgi_param DOCUMENT_ROOT $document_root; +scgi_param SCGI 1; +scgi_param SERVER_PROTOCOL $server_protocol; + +scgi_param REMOTE_ADDR $remote_addr; +scgi_param REMOTE_PORT $remote_port; +scgi_param SERVER_PORT $server_port; +scgi_param SERVER_NAME $server_name; diff --git a/letsencrypt/client/plugins/nginx/tests/testdata/ubuntu_nginx_1_4_6/default_vhost/nginx/sites-available/default b/letsencrypt/client/plugins/nginx/tests/testdata/ubuntu_nginx_1_4_6/default_vhost/nginx/sites-available/default new file mode 100644 index 000000000..5d8f3ac15 --- /dev/null +++ b/letsencrypt/client/plugins/nginx/tests/testdata/ubuntu_nginx_1_4_6/default_vhost/nginx/sites-available/default @@ -0,0 +1,112 @@ +# You may add here your +# server { +# ... +# } +# statements for each of your virtual hosts to this file + +## +# You should look at the following URL's in order to grasp a solid understanding +# of Nginx configuration files in order to fully unleash the power of Nginx. +# http://wiki.nginx.org/Pitfalls +# http://wiki.nginx.org/QuickStart +# http://wiki.nginx.org/Configuration +# +# Generally, you will want to move this file somewhere, and start with a clean +# file but keep this around for reference. Or just disable in sites-enabled. +# +# Please see /usr/share/doc/nginx-doc/examples/ for more detailed examples. +## + +server { + listen 80 default_server; + listen [::]:80 default_server ipv6only=on; + + root /usr/share/nginx/html; + index index.html index.htm; + + # Make site accessible from http://localhost/ + server_name localhost; + + location / { + # First attempt to serve request as file, then + # as directory, then fall back to displaying a 404. + try_files $uri $uri/ =404; + # Uncomment to enable naxsi on this location + # include /etc/nginx/naxsi.rules + } + + # Only for nginx-naxsi used with nginx-naxsi-ui : process denied requests + #location /RequestDenied { + # proxy_pass http://127.0.0.1:8080; + #} + + #error_page 404 /404.html; + + # redirect server error pages to the static page /50x.html + # + #error_page 500 502 503 504 /50x.html; + #location = /50x.html { + # root /usr/share/nginx/html; + #} + + # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000 + # + #location ~ \.php$ { + # fastcgi_split_path_info ^(.+\.php)(/.+)$; + # # NOTE: You should have "cgi.fix_pathinfo = 0;" in php.ini + # + # # With php5-cgi alone: + # fastcgi_pass 127.0.0.1:9000; + # # With php5-fpm: + # fastcgi_pass unix:/var/run/php5-fpm.sock; + # fastcgi_index index.php; + # include fastcgi_params; + #} + + # deny access to .htaccess files, if Apache's document root + # concurs with nginx's one + # + #location ~ /\.ht { + # deny all; + #} +} + + +# another virtual host using mix of IP-, name-, and port-based configuration +# +#server { +# listen 8000; +# listen somename:8080; +# server_name somename alias another.alias; +# root html; +# index index.html index.htm; +# +# location / { +# try_files $uri $uri/ =404; +# } +#} + + +# HTTPS server +# +#server { +# listen 443; +# server_name localhost; +# +# root html; +# index index.html index.htm; +# +# ssl on; +# ssl_certificate cert.pem; +# ssl_certificate_key cert.key; +# +# ssl_session_timeout 5m; +# +# ssl_protocols SSLv3 TLSv1 TLSv1.1 TLSv1.2; +# ssl_ciphers "HIGH:!aNULL:!MD5 or HIGH:!aNULL:!MD5:!3DES"; +# ssl_prefer_server_ciphers on; +# +# location / { +# try_files $uri $uri/ =404; +# } +#} diff --git a/letsencrypt/client/plugins/nginx/tests/testdata/ubuntu_nginx_1_4_6/default_vhost/nginx/sites-enabled/default b/letsencrypt/client/plugins/nginx/tests/testdata/ubuntu_nginx_1_4_6/default_vhost/nginx/sites-enabled/default new file mode 120000 index 000000000..ad35b8342 --- /dev/null +++ b/letsencrypt/client/plugins/nginx/tests/testdata/ubuntu_nginx_1_4_6/default_vhost/nginx/sites-enabled/default @@ -0,0 +1 @@ +/etc/nginx/sites-available/default \ No newline at end of file diff --git a/letsencrypt/client/plugins/nginx/tests/testdata/ubuntu_nginx_1_4_6/default_vhost/nginx/uwsgi_params b/letsencrypt/client/plugins/nginx/tests/testdata/ubuntu_nginx_1_4_6/default_vhost/nginx/uwsgi_params new file mode 100644 index 000000000..3f72dbf0e --- /dev/null +++ b/letsencrypt/client/plugins/nginx/tests/testdata/ubuntu_nginx_1_4_6/default_vhost/nginx/uwsgi_params @@ -0,0 +1,15 @@ +uwsgi_param QUERY_STRING $query_string; +uwsgi_param REQUEST_METHOD $request_method; +uwsgi_param CONTENT_TYPE $content_type; +uwsgi_param CONTENT_LENGTH $content_length; + +uwsgi_param REQUEST_URI $request_uri; +uwsgi_param PATH_INFO $document_uri; +uwsgi_param DOCUMENT_ROOT $document_root; +uwsgi_param SERVER_PROTOCOL $server_protocol; +uwsgi_param UWSGI_SCHEME $scheme; + +uwsgi_param REMOTE_ADDR $remote_addr; +uwsgi_param REMOTE_PORT $remote_port; +uwsgi_param SERVER_PORT $server_port; +uwsgi_param SERVER_NAME $server_name; diff --git a/letsencrypt/client/plugins/nginx/tests/testdata/ubuntu_nginx_1_4_6/default_vhost/nginx/win-utf b/letsencrypt/client/plugins/nginx/tests/testdata/ubuntu_nginx_1_4_6/default_vhost/nginx/win-utf new file mode 100644 index 000000000..774fd9fc9 --- /dev/null +++ b/letsencrypt/client/plugins/nginx/tests/testdata/ubuntu_nginx_1_4_6/default_vhost/nginx/win-utf @@ -0,0 +1,125 @@ +# This map is not a full windows-1251 <> utf8 map: it does not +# contain Serbian and Macedonian letters. If you need a full map, +# use contrib/unicode2nginx/win-utf map instead. + +charset_map windows-1251 utf-8 { + + 82 E2809A; # single low-9 quotation mark + + 84 E2809E; # double low-9 quotation mark + 85 E280A6; # ellipsis + 86 E280A0; # dagger + 87 E280A1; # double dagger + 88 E282AC; # euro + 89 E280B0; # per mille + + 91 E28098; # left single quotation mark + 92 E28099; # right single quotation mark + 93 E2809C; # left double quotation mark + 94 E2809D; # right double quotation mark + 95 E280A2; # bullet + 96 E28093; # en dash + 97 E28094; # em dash + + 99 E284A2; # trade mark sign + + A0 C2A0; #   + A1 D18E; # capital Byelorussian short U + A2 D19E; # small Byelorussian short u + + A4 C2A4; # currency sign + A5 D290; # capital Ukrainian soft G + A6 C2A6; # borken bar + A7 C2A7; # section sign + A8 D081; # capital YO + A9 C2A9; # (C) + AA D084; # capital Ukrainian YE + AB C2AB; # left-pointing double angle quotation mark + AC C2AC; # not sign + AD C2AD; # soft hypen + AE C2AE; # (R) + AF D087; # capital Ukrainian YI + + B0 C2B0; # ° + B1 C2B1; # plus-minus sign + B2 D086; # capital Ukrainian I + B3 D196; # small Ukrainian i + B4 D291; # small Ukrainian soft g + B5 C2B5; # micro sign + B6 C2B6; # pilcrow sign + B7 C2B7; # · + B8 D191; # small yo + B9 E28496; # numero sign + BA D194; # small Ukrainian ye + BB C2BB; # right-pointing double angle quotation mark + + BF D197; # small Ukrainian yi + + C0 D090; # capital A + C1 D091; # capital B + C2 D092; # capital V + C3 D093; # capital G + C4 D094; # capital D + C5 D095; # capital YE + C6 D096; # capital ZH + C7 D097; # capital Z + C8 D098; # capital I + C9 D099; # capital J + CA D09A; # capital K + CB D09B; # capital L + CC D09C; # capital M + CD D09D; # capital N + CE D09E; # capital O + CF D09F; # capital P + + D0 D0A0; # capital R + D1 D0A1; # capital S + D2 D0A2; # capital T + D3 D0A3; # capital U + D4 D0A4; # capital F + D5 D0A5; # capital KH + D6 D0A6; # capital TS + D7 D0A7; # capital CH + D8 D0A8; # capital SH + D9 D0A9; # capital SHCH + DA D0AA; # capital hard sign + DB D0AB; # capital Y + DC D0AC; # capital soft sign + DD D0AD; # capital E + DE D0AE; # capital YU + DF D0AF; # capital YA + + E0 D0B0; # small a + E1 D0B1; # small b + E2 D0B2; # small v + E3 D0B3; # small g + E4 D0B4; # small d + E5 D0B5; # small ye + E6 D0B6; # small zh + E7 D0B7; # small z + E8 D0B8; # small i + E9 D0B9; # small j + EA D0BA; # small k + EB D0BB; # small l + EC D0BC; # small m + ED D0BD; # small n + EE D0BE; # small o + EF D0BF; # small p + + F0 D180; # small r + F1 D181; # small s + F2 D182; # small t + F3 D183; # small u + F4 D184; # small f + F5 D185; # small kh + F6 D186; # small ts + F7 D187; # small ch + F8 D188; # small sh + F9 D189; # small shch + FA D18A; # small hard sign + FB D18B; # small y + FC D18C; # small soft sign + FD D18D; # small e + FE D18E; # small yu + FF D18F; # small ya +} From 1505f5e7bca4900814232d79c499a2a7d6fed013 Mon Sep 17 00:00:00 2001 From: yan Date: Thu, 16 Apr 2015 17:51:45 -0700 Subject: [PATCH 31/38] Empty format field not allowed in python 2.6 --- letsencrypt/client/plugins/nginx/configurator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/letsencrypt/client/plugins/nginx/configurator.py b/letsencrypt/client/plugins/nginx/configurator.py index 38006e742..7fed3f9a2 100644 --- a/letsencrypt/client/plugins/nginx/configurator.py +++ b/letsencrypt/client/plugins/nginx/configurator.py @@ -290,7 +290,7 @@ class NginxConfigurator(object): self.choose_vhost(domain), options) except (KeyError, ValueError): raise errors.LetsEncryptConfiguratorError( - "Unsupported enhancement: {}".format(enhancement)) + "Unsupported enhancement: {0}".format(enhancement)) except errors.LetsEncryptConfiguratorError: logging.warn("Failed %s for %s", enhancement, domain) From 995b5622f839c82e99ae4bf8fbd3ea07258bb95d Mon Sep 17 00:00:00 2001 From: yan Date: Fri, 17 Apr 2015 17:05:00 -0700 Subject: [PATCH 32/38] Fix most pylint errors --- .gitignore | 1 + .../client/plugins/nginx/configurator.py | 7 +- letsencrypt/client/plugins/nginx/dvsni.py | 6 - .../client/plugins/nginx/nginxparser.py | 34 +++ letsencrypt/client/plugins/nginx/obj.py | 2 +- letsencrypt/client/plugins/nginx/parser.py | 202 +++++++++--------- .../plugins/nginx/tests/configurator_test.py | 16 +- .../plugins/nginx/tests/nginxparser_test.py | 23 +- .../client/plugins/nginx/tests/obj_test.py | 8 +- .../client/plugins/nginx/tests/parser_test.py | 42 ++-- .../client/plugins/nginx/tests/util.py | 2 +- 11 files changed, 191 insertions(+), 152 deletions(-) diff --git a/.gitignore b/.gitignore index e2ec0622c..51164db97 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ *.pyc *.egg-info +.eggs/ build/ dist/ venv/ diff --git a/letsencrypt/client/plugins/nginx/configurator.py b/letsencrypt/client/plugins/nginx/configurator.py index 7fed3f9a2..2caec77dc 100644 --- a/letsencrypt/client/plugins/nginx/configurator.py +++ b/letsencrypt/client/plugins/nginx/configurator.py @@ -89,6 +89,7 @@ class NginxConfigurator(object): # Entry point in main.py for installing cert def deploy_cert(self, domain, cert, key, cert_chain=None): + # pylint: disable=unused-argument """Deploys certificate to specified virtual host. Aborts if the vhost is missing ssl_certificate or ssl_certificate_key. @@ -383,9 +384,9 @@ class NginxConfigurator(object): nginx_version = tuple([int(i) for i in version_matches[0].split(".")]) # nginx < 0.8.21 doesn't use default_server - if (nginx_version[0] == 0 and - (nginx_version[1] < 8 or - (nginx_version[1] == 8 and nginx_version[2] < 21))): + if (nginx_version[0] == 0 and (nginx_version[1] < 8 or + (nginx_version[1] == 8 and + nginx_version[2] < 21))): raise errors.LetsEncryptConfiguratorError( "Nginx version must be 0.8.21+") diff --git a/letsencrypt/client/plugins/nginx/dvsni.py b/letsencrypt/client/plugins/nginx/dvsni.py index cd0a7ba5d..450dcf800 100644 --- a/letsencrypt/client/plugins/nginx/dvsni.py +++ b/letsencrypt/client/plugins/nginx/dvsni.py @@ -30,15 +30,9 @@ class NginxDvsni(object): VHOST_TEMPLATE = """\ ServerName {server_name} - UseCanonicalName on - SSLStrictSNIVHostCheck on - - LimitRequestBody 1048576 - Include {ssl_options_conf_path} SSLCertificateFile {cert_path} SSLCertificateKeyFile {key_path} - DocumentRoot {document_root} diff --git a/letsencrypt/client/plugins/nginx/nginxparser.py b/letsencrypt/client/plugins/nginx/nginxparser.py index 8f995cf61..947c05f2e 100644 --- a/letsencrypt/client/plugins/nginx/nginxparser.py +++ b/letsencrypt/client/plugins/nginx/nginxparser.py @@ -7,6 +7,7 @@ from pyparsing import ( class RawNginxParser(object): + # pylint: disable=expression-not-assigned """ A class that parses nginx configuration with pyparsing """ @@ -51,6 +52,7 @@ class RawNginxParser(object): class RawNginxDumper(object): + # pylint: disable=too-few-public-methods """ A class that dumps nginx configuration from the provided tree. """ @@ -86,6 +88,9 @@ class RawNginxDumper(object): yield spacer * current_indent + key + spacer + values + ';' def as_string(self): + """ + Return the parsed block as a string. + """ return '\n'.join(self) @@ -93,16 +98,45 @@ class RawNginxDumper(object): # (like pyyaml, picker or json) def loads(source): + """Parses from a string. + + :param str souce: The string to parse + :returns: The parsed tree + :rtype: list + + """ return RawNginxParser(source).as_list() def load(_file): + """Parses from a file. + + :param file _file: The file to parse + :returns: The parsed tree + :rtype: list + + """ return loads(_file.read()) def dumps(blocks, indentation=4): + """Dump to a string. + + :param list block: The parsed tree + :param int indentation: The number of spaces to indent + :rtype: str + + """ return RawNginxDumper(blocks, indentation).as_string() def dump(blocks, _file, indentation=4): + """Dump to a file. + + :param list block: The parsed tree + :param file _file: The file to dump to + :param int indentation: The number of spaces to indent + :rtype: NoneType + + """ return _file.write(dumps(blocks, indentation)) diff --git a/letsencrypt/client/plugins/nginx/obj.py b/letsencrypt/client/plugins/nginx/obj.py index 8013ed2c8..3509c16f9 100644 --- a/letsencrypt/client/plugins/nginx/obj.py +++ b/letsencrypt/client/plugins/nginx/obj.py @@ -45,7 +45,7 @@ class Addr(object): return None tup = addr.partition(':') - if re.match('^\d+$', tup[0]): + if re.match(r'^\d+$', tup[0]): # This is a bare port, not a hostname. E.g. listen 80 host = '' port = tup[0] diff --git a/letsencrypt/client/plugins/nginx/parser.py b/letsencrypt/client/plugins/nginx/parser.py index 4c6d40662..dca022022 100644 --- a/letsencrypt/client/plugins/nginx/parser.py +++ b/letsencrypt/client/plugins/nginx/parser.py @@ -50,19 +50,19 @@ class NginxParser(object): trees = self._parse_files(filepath) for tree in trees: for entry in tree: - if self._is_include_directive(entry): + if _is_include_directive(entry): # Parse the top-level included file self._parse_recursively(entry[1]) elif entry[0] == ['http'] or entry[0] == ['server']: # Look for includes in the top-level 'http'/'server' context for subentry in entry[1]: - if self._is_include_directive(subentry): + if _is_include_directive(subentry): self._parse_recursively(subentry[1]) elif entry[0] == ['http'] and subentry[0] == ['server']: # Look for includes in a 'server' context within # an 'http' context for server_entry in subentry[1]: - if self._is_include_directive(server_entry): + if _is_include_directive(server_entry): self._parse_recursively(server_entry[1]) def abs_path(self, path): @@ -79,19 +79,8 @@ class NginxParser(object): else: return path - def _is_include_directive(self, entry): - """Checks if an nginx parsed entry is an 'include' directive. - - :param list entry: the parsed entry - :returns: Whether it's an 'include' directive - :rtype: bool - - """ - return (type(entry) == list and - entry[0] == 'include' and len(entry) == 2 and - type(entry[1]) == str) - def get_vhosts(self): + # pylint: disable=cell-var-from-loop """Gets list of all 'virtual hosts' found in Nginx configuration. Technically this is a misnomer because Nginx does not have virtual hosts, it has 'server blocks'. @@ -109,10 +98,11 @@ class NginxParser(object): for filename in self.parsed: tree = self.parsed[filename] servers[filename] = [] + srv = servers[filename] # workaround undefined loop var in lambdas # Find all the server blocks _do_for_subarray(tree, lambda x: x[0] == ['server'], - lambda x: servers[filename].append(x[1])) + lambda x: srv.append(x[1])) # Find 'include' statements in server blocks and append their trees for i, server in enumerate(servers[filename]): @@ -122,7 +112,7 @@ class NginxParser(object): for filename in servers: for server in servers[filename]: # Parse the server block into a VirtualHost object - parsed_server = self._parse_server(server) + parsed_server = _parse_server(server) vhost = obj.VirtualHost(filename, parsed_server['addrs'], parsed_server['ssl'], @@ -143,51 +133,16 @@ class NginxParser(object): """ result = list(block) # Copy the list to keep self.parsed idempotent for directive in block: - if (self._is_include_directive(directive)): + if _is_include_directive(directive): included_files = glob.glob( self.abs_path(directive[1])) - for f in included_files: + for incl in included_files: try: - result.extend(self.parsed[f]) - except: + result.extend(self.parsed[incl]) + except KeyError: pass return result - def _parse_server(self, server): - """Parses a list of server directives. - - :param list server: list of directives in a server block - :rtype: dict - - """ - parsed_server = {} - parsed_server['addrs'] = set() - parsed_server['ssl'] = False - parsed_server['names'] = set() - - for directive in server: - if directive[0] == 'listen': - addr = obj.Addr.fromstring(directive[1]) - parsed_server['addrs'].add(addr) - if not parsed_server['ssl'] and addr.ssl: - parsed_server['ssl'] = True - elif directive[0] == 'server_name': - parsed_server['names'].update( - self._get_servernames(directive[1])) - - return parsed_server - - def _get_servernames(self, names): - """Turns a server_name string into a list of server names - - :param str names: server names - :rtype: list - - """ - whitespace_re = re.compile(r'\s+') - names = re.sub(whitespace_re, ' ', names) - return names.split(' ') - def _parse_files(self, filepath, override=False): """Parse files from a glob @@ -199,18 +154,18 @@ class NginxParser(object): """ files = glob.glob(filepath) trees = [] - for f in files: - if f in self.parsed and not override: + for item in files: + if item in self.parsed and not override: continue try: - with open(f) as fo: - parsed = load(fo) - self.parsed[f] = parsed + with open(item) as _file: + parsed = load(_file) + self.parsed[item] = parsed trees.append(parsed) except IOError: - logging.warn("Could not open file: %s" % f) + logging.warn("Could not open file: %s", item) except pyparsing.ParseException: - logging.warn("Could not parse file: %s" % f) + logging.warn("Could not parse file: %s", item) return trees def _set_locations(self, ssl_options): @@ -257,10 +212,10 @@ class NginxParser(object): if ext: filename = filename + os.path.extsep + ext try: - with open(filename, 'w') as f: - dump(tree, f) + with open(filename, 'w') as _file: + dump(tree, _file) except IOError: - logging.error("Could not open file for writing: %s" % filename) + logging.error("Could not open file for writing: %s", filename) def _has_server_names(self, entry, names): """Checks if a server block has the given set of server_names. This @@ -279,44 +234,22 @@ class NginxParser(object): # Nothing to identify blocks with return False - if type(entry) != list: + if not isinstance(entry, list): # Can't be a server block return False new_entry = self._get_included_directives(entry) server_names = set() for item in new_entry: - if type(item) != list: + if not isinstance(item, list): # Can't be a server block return False if item[0] == 'server_name': - server_names.update(self._get_servernames(item[1])) + server_names.update(_get_servernames(item[1])) return server_names == names - def _replace_directives(self, block, directives): - """Replaces directives in a block. If the directive doesn't exist in - the entry already, raises a misconfiguration error. - - ..todo :: Find directives that are in included files. - - :param list block: The block to replace in - :param list directives: The new directives. - """ - for directive in directives: - changed = False - if len(directive) == 0: - continue - for index, line in enumerate(block): - if len(line) > 0 and line[0] == directive[0]: - block[index] = directive - changed = True - if not changed: - raise errors.LetsEncryptMisconfigurationError( - 'LetsEncrypt expected directive for %s in the Nginx config ' - 'but did not find it.' % directive[0]) - def add_server_directives(self, filename, names, directives, replace=False): """Add or replace directives in server blocks whose server_name set @@ -335,7 +268,7 @@ class NginxParser(object): if replace: _do_for_subarray(self.parsed[filename], lambda x: self._has_server_names(x, names), - lambda x: self._replace_directives(x, directives)) + lambda x: _replace_directives(x, directives)) else: _do_for_subarray(self.parsed[filename], lambda x: self._has_server_names(x, names), @@ -375,7 +308,7 @@ def _do_for_subarray(entry, condition, func): :param function func: The function to call for each matching item """ - if type(entry) == list: + if isinstance(entry, list): if condition(entry): func(entry) else: @@ -411,15 +344,15 @@ def get_best_match(target_name, names): if len(exact) > 0: # There can be more than one exact match; e.g. eff.org, .eff.org - match = min(exact, key=lambda x: len(x)) + match = min(exact, key=len) return ('exact', match) if len(wildcard_start) > 0: # Return the longest wildcard - match = max(wildcard_start, key=lambda x: len(x)) + match = max(wildcard_start, key=len) return ('wildcard_start', match) if len(wildcard_end) > 0: # Return the longest wildcard - match = max(wildcard_end, key=lambda x: len(x)) + match = max(wildcard_end, key=len) return ('wildcard_end', match) if len(regex) > 0: # Just return the first one for now @@ -430,7 +363,7 @@ def get_best_match(target_name, names): def _exact_match(target_name, name): - return (target_name == name or '.' + target_name == name) + return target_name == name or '.' + target_name == name def _wildcard_match(target_name, name, start): @@ -473,6 +406,79 @@ def _regex_match(target_name, name): return True else: return False - except: + except re.error: # perl-compatible regexes are sometimes not recognized by python return False + + +def _is_include_directive(entry): + """Checks if an nginx parsed entry is an 'include' directive. + + :param list entry: the parsed entry + :returns: Whether it's an 'include' directive + :rtype: bool + + """ + return (isinstance(entry, list) and + entry[0] == 'include' and len(entry) == 2 and + isinstance(entry[1], str)) + + +def _get_servernames(names): + """Turns a server_name string into a list of server names + + :param str names: server names + :rtype: list + + """ + whitespace_re = re.compile(r'\s+') + names = re.sub(whitespace_re, ' ', names) + return names.split(' ') + + +def _parse_server(server): + """Parses a list of server directives. + + :param list server: list of directives in a server block + :rtype: dict + + """ + parsed_server = {} + parsed_server['addrs'] = set() + parsed_server['ssl'] = False + parsed_server['names'] = set() + + for directive in server: + if directive[0] == 'listen': + addr = obj.Addr.fromstring(directive[1]) + parsed_server['addrs'].add(addr) + if not parsed_server['ssl'] and addr.ssl: + parsed_server['ssl'] = True + elif directive[0] == 'server_name': + parsed_server['names'].update( + _get_servernames(directive[1])) + + return parsed_server + + +def _replace_directives(block, directives): + """Replaces directives in a block. If the directive doesn't exist in + the entry already, raises a misconfiguration error. + + ..todo :: Find directives that are in included files. + + :param list block: The block to replace in + :param list directives: The new directives. + """ + for directive in directives: + changed = False + if len(directive) == 0: + continue + for index, line in enumerate(block): + if len(line) > 0 and line[0] == directive[0]: + block[index] = directive + changed = True + if not changed: + raise errors.LetsEncryptMisconfigurationError( + 'LetsEncrypt expected directive for %s in the Nginx config ' + 'but did not find it.' % directive[0]) diff --git a/letsencrypt/client/plugins/nginx/tests/configurator_test.py b/letsencrypt/client/plugins/nginx/tests/configurator_test.py index fda3bad05..35c2573ef 100644 --- a/letsencrypt/client/plugins/nginx/tests/configurator_test.py +++ b/letsencrypt/client/plugins/nginx/tests/configurator_test.py @@ -36,7 +36,7 @@ class NginxConfiguratorTest(util.NginxTest): names = self.config.get_all_names() self.assertEqual(names, set( ["*.www.foo.com", "somename", "another.alias", - "alias", "localhost", ".example.com", "~^(www\.)?(example|bar)\.", + "alias", "localhost", ".example.com", r"~^(www\.)?(example|bar)\.", "155.225.50.69.nephoscale.net", "*.www.example.com", "example.*", "www.example.org", "myhost"])) @@ -70,7 +70,7 @@ class NginxConfiguratorTest(util.NginxTest): parsed[0]) def test_choose_vhost(self): - localhost_conf = set(['localhost', '~^(www\.)?(example|bar)\.']) + localhost_conf = set(['localhost', r'~^(www\.)?(example|bar)\.']) server_conf = set(['somename', 'another.alias', 'alias']) example_conf = set(['.example.com', 'example.*']) foo_conf = set(['*.www.foo.com', '*.www.example.com']) @@ -225,17 +225,17 @@ class NginxConfiguratorTest(util.NginxTest): @mock.patch("letsencrypt.client.plugins.nginx.configurator." "subprocess.Popen") def test_nginx_restart(self, mock_popen): - m = mock_popen() - m.communicate.return_value = ('', '') - m.returncode = 0 + mocked = mock_popen() + mocked.communicate.return_value = ('', '') + mocked.returncode = 0 self.assertTrue(self.config.restart()) @mock.patch("letsencrypt.client.plugins.nginx.configurator." "subprocess.Popen") def test_config_test(self, mock_popen): - m = mock_popen() - m.communicate.return_value = ('', '') - m.returncode = 0 + mocked = mock_popen() + mocked.communicate.return_value = ('', '') + mocked.returncode = 0 self.assertTrue(self.config.config_test()) if __name__ == "__main__": diff --git a/letsencrypt/client/plugins/nginx/tests/nginxparser_test.py b/letsencrypt/client/plugins/nginx/tests/nginxparser_test.py index 5f0601db3..b249b25cc 100644 --- a/letsencrypt/client/plugins/nginx/tests/nginxparser_test.py +++ b/letsencrypt/client/plugins/nginx/tests/nginxparser_test.py @@ -1,3 +1,4 @@ +"""Test for letsencrypt.client.plugins.nginx.nginxparser.""" import operator import unittest @@ -6,10 +7,11 @@ from letsencrypt.client.plugins.nginx.nginxparser import (RawNginxParser, from letsencrypt.client.plugins.nginx.tests import util -first = operator.itemgetter(0) +FIRST = operator.itemgetter(0) class TestRawNginxParser(unittest.TestCase): + """Test the raw low-level Nginx config parser.""" def test_assignments(self): parsed = RawNginxParser.assignment.parseString('root /test;').asList() @@ -28,8 +30,9 @@ class TestRawNginxParser(unittest.TestCase): def test_nested_blocks(self): parsed = RawNginxParser.block.parseString('foo { bar {} }').asList() - block, content = first(parsed) - self.assertEqual(first(content), [['bar'], []]) + block, content = FIRST(parsed) + self.assertEqual(FIRST(content), [['bar'], []]) + self.assertEqual(FIRST(block), 'foo') def test_dump_as_string(self): dumped = dumps([ @@ -71,13 +74,13 @@ class TestRawNginxParser(unittest.TestCase): [['location', '/status'], [ [['types'], [['image/jpeg', 'jpg']]], ]], - [['location', '~', 'case_sensitive\.php$'], [ + [['location', '~', r'case_sensitive\.php$'], [ ['index', 'index.php'], ['root', '/var/root'], ]], - [['location', '~*', 'case_insensitive\.php$'], []], - [['location', '=', 'exact_match\.php$'], []], - [['location', '^~', 'ignore_regex\.php$'], []] + [['location', '~*', r'case_insensitive\.php$'], []], + [['location', '=', r'exact_match\.php$'], []], + [['location', '^~', r'ignore_regex\.php$'], []] ]]]]] ) @@ -94,9 +97,9 @@ class TestRawNginxParser(unittest.TestCase): [['location', '/'], [['root', 'html'], ['index', 'index.html index.htm']]]]]) - f = open(util.get_data_filename('nginx.new.conf'), 'w') - dump(parsed, f) - f.close() + _file = open(util.get_data_filename('nginx.new.conf'), 'w') + dump(parsed, _file) + _file.close() parsed_new = load(open(util.get_data_filename('nginx.new.conf'))) self.assertEquals(parsed, parsed_new) diff --git a/letsencrypt/client/plugins/nginx/tests/obj_test.py b/letsencrypt/client/plugins/nginx/tests/obj_test.py index d4c47ca32..d5591c763 100644 --- a/letsencrypt/client/plugins/nginx/tests/obj_test.py +++ b/letsencrypt/client/plugins/nginx/tests/obj_test.py @@ -95,10 +95,10 @@ class VirtualHostTest(unittest.TestCase): self.assertFalse(vhost1b == 1234) def test_str(self): - s = '\n'.join(['file: filep', 'addrs: localhost', - "names: set(['localhost'])", 'ssl: False', - 'enabled: False']) - self.assertEqual(s, str(self.vhost1)) + stringified = '\n'.join(['file: filep', 'addrs: localhost', + "names: set(['localhost'])", 'ssl: False', + 'enabled: False']) + self.assertEqual(stringified, str(self.vhost1)) if __name__ == "__main__": diff --git a/letsencrypt/client/plugins/nginx/tests/parser_test.py b/letsencrypt/client/plugins/nginx/tests/parser_test.py index 36aef9f63..a76f2da25 100644 --- a/letsencrypt/client/plugins/nginx/tests/parser_test.py +++ b/letsencrypt/client/plugins/nginx/tests/parser_test.py @@ -43,10 +43,10 @@ class NginxParserTest(util.NginxTest): """ parser = NginxParser(self.config_path, self.ssl_options) parser.load() - self.assertEqual(set(map(parser.abs_path, - ['foo.conf', 'nginx.conf', 'server.conf', - 'sites-enabled/default', - 'sites-enabled/example.com'])), + self.assertEqual(set([parser.abs_path(x) for x in + ['foo.conf', 'nginx.conf', 'server.conf', + 'sites-enabled/default', + 'sites-enabled/example.com']]), set(parser.parsed.keys())) self.assertEqual([['server_name', 'somename alias another.alias']], parser.parsed[parser.abs_path('server.conf')]) @@ -85,7 +85,7 @@ class NginxParserTest(util.NginxTest): vhost1 = VirtualHost(parser.abs_path('nginx.conf'), [Addr('', '8080', False, False)], False, True, set(['localhost', - '~^(www\.)?(example|bar)\.']), + r'~^(www\.)?(example|bar)\.']), []) vhost2 = VirtualHost(parser.abs_path('nginx.conf'), [Addr('somename', '8080', False, False), @@ -106,26 +106,26 @@ class NginxParserTest(util.NginxTest): '*.www.example.com']), []) self.assertEqual(5, len(vhosts)) - example_com = filter(lambda x: 'example.com' in x.filep, vhosts)[0] + example_com = [x for x in vhosts if 'example.com' in x.filep][0] self.assertEqual(vhost3, example_com) - default = filter(lambda x: 'default' in x.filep, vhosts)[0] + default = [x for x in vhosts if 'default' in x.filep][0] self.assertEqual(vhost4, default) - foo = filter(lambda x: 'foo.conf' in x.filep, vhosts)[0] - self.assertEqual(vhost5, foo) - localhost = filter(lambda x: 'localhost' in x.names, vhosts)[0] + fooconf = [x for x in vhosts if 'foo.conf' in x.filep][0] + self.assertEqual(vhost5, fooconf) + localhost = [x for x in vhosts if 'localhost' in x.names][0] self.assertEquals(vhost1, localhost) - somename = filter(lambda x: 'somename' in x.names, vhosts)[0] + somename = [x for x in vhosts if 'somename' in x.names][0] self.assertEquals(vhost2, somename) def test_add_server_directives(self): parser = NginxParser(self.config_path, self.ssl_options) parser.add_server_directives(parser.abs_path('nginx.conf'), set(['localhost', - '~^(www\.)?(example|bar)\.']), + r'~^(www\.)?(example|bar)\.']), [['foo', 'bar'], ['ssl_certificate', '/etc/ssl/cert.pem']]) - r = re.compile('foo bar;\n\s+ssl_certificate /etc/ssl/cert.pem') - self.assertEqual(1, len(re.findall(r, dumps(parser.parsed[ + ssl_re = re.compile(r'foo bar;\n\s+ssl_certificate /etc/ssl/cert.pem') + self.assertEqual(1, len(re.findall(ssl_re, dumps(parser.parsed[ parser.abs_path('nginx.conf')])))) parser.add_server_directives(parser.abs_path('server.conf'), set(['alias', 'another.alias', @@ -161,10 +161,10 @@ class NginxParserTest(util.NginxTest): set(['*.eff.org', '.www.eff.org']), set(['.eff.org', '*.org']), set(['www.eff.', 'www.eff.*', '*.www.eff.org']), - set(['example.com', '~^(www\.)?(eff.+)', '*.eff.*']), - set(['*', '~^(www\.)?(eff.+)']), - set(['www.*', '~^(www\.)?(eff.+)', '.test.eff.org']), - set(['*.org', '*.eff.org', 'www.eff.*']), + set(['example.com', r'~^(www\.)?(eff.+)', '*.eff.*']), + set(['*', r'~^(www\.)?(eff.+)']), + set(['www.*', r'~^(www\.)?(eff.+)', '.test.eff.org']), + set(['*.org', r'*.eff.org', 'www.eff.*']), set(['*.www.eff.org', 'www.*']), set(['*.org']), set([]), @@ -174,7 +174,7 @@ class NginxParserTest(util.NginxTest): ('exact', '.www.eff.org'), ('wildcard_start', '.eff.org'), ('wildcard_end', 'www.eff.*'), - ('regex', '~^(www\.)?(eff.+)'), + ('regex', r'~^(www\.)?(eff.+)'), ('wildcard_start', '*'), ('wildcard_end', 'www.*'), ('wildcard_start', '*.eff.org'), @@ -194,8 +194,8 @@ class NginxParserTest(util.NginxTest): [['ssl_certificate', 'foo.pem'], ['ssl_certificate_key', 'bar.key'], ['listen', '443 ssl']]) - ck = parser.get_all_certs_keys() - self.assertEqual(set([('foo.pem', 'bar.key', filep)]), ck) + c_k = parser.get_all_certs_keys() + self.assertEqual(set([('foo.pem', 'bar.key', filep)]), c_k) if __name__ == "__main__": diff --git a/letsencrypt/client/plugins/nginx/tests/util.py b/letsencrypt/client/plugins/nginx/tests/util.py index 205e511af..4a4502379 100644 --- a/letsencrypt/client/plugins/nginx/tests/util.py +++ b/letsencrypt/client/plugins/nginx/tests/util.py @@ -15,7 +15,6 @@ class NginxTest(unittest.TestCase): # pylint: disable=too-few-public-methods def setUp(self): super(NginxTest, self).setUp() - self.maxDiff = None self.temp_dir, self.config_dir, self.work_dir = dir_setup( "testdata") @@ -32,6 +31,7 @@ class NginxTest(unittest.TestCase): # pylint: disable=too-few-public-methods def get_data_filename(filename): + """Gets the filename of a test data file.""" return pkg_resources.resource_filename( "letsencrypt.client.plugins.nginx.tests", "testdata/%s" % filename) From f3126e77a714ae8acd04e49c0cf4b6e74463cf35 Mon Sep 17 00:00:00 2001 From: yan Date: Fri, 17 Apr 2015 17:57:04 -0700 Subject: [PATCH 33/38] Fix duplicate code lint errors --- .../client/plugins/nginx/configurator.py | 4 +- letsencrypt/client/plugins/nginx/dvsni.py | 45 +++++-------------- letsencrypt/client/plugins/nginx/obj.py | 17 ++----- letsencrypt/client/plugins/nginx/parser.py | 8 ++-- .../plugins/nginx/tests/configurator_test.py | 16 +++---- .../client/plugins/nginx/tests/dvsni_test.py | 12 ++--- .../client/plugins/nginx/tests/util.py | 13 ++---- 7 files changed, 40 insertions(+), 75 deletions(-) diff --git a/letsencrypt/client/plugins/nginx/configurator.py b/letsencrypt/client/plugins/nginx/configurator.py index 2caec77dc..d799432f3 100644 --- a/letsencrypt/client/plugins/nginx/configurator.py +++ b/letsencrypt/client/plugins/nginx/configurator.py @@ -338,13 +338,13 @@ class NginxConfigurator(object): Make sure that files/directories are setup with appropriate permissions Aim for defensive coding... make sure all input files - have permissions of root + have permissions of root. """ uid = os.geteuid() - le_util.make_or_verify_dir(self.config.config_dir, 0o755, uid) le_util.make_or_verify_dir(self.config.work_dir, 0o755, uid) le_util.make_or_verify_dir(self.config.backup_dir, 0o755, uid) + le_util.make_or_verify_dir(self.config.config_dir, 0o755, uid) def get_version(self): """Return version of Nginx Server. diff --git a/letsencrypt/client/plugins/nginx/dvsni.py b/letsencrypt/client/plugins/nginx/dvsni.py index 450dcf800..9535a90c7 100644 --- a/letsencrypt/client/plugins/nginx/dvsni.py +++ b/letsencrypt/client/plugins/nginx/dvsni.py @@ -1,9 +1,10 @@ """NginxDVSNI""" import logging -import os + +from letsencrypt.client.plugins.apache.dvsni import ApacheDvsni -class NginxDvsni(object): +class NginxDvsni(ApacheDvsni): """Class performs DVSNI challenges within the Nginx configurator. .. todo:: This is basically copied-and-pasted from the Apache equivalent. @@ -38,51 +39,29 @@ class NginxDvsni(object): """ - def __init__(self, configurator): - self.configurator = configurator - self.achalls = [] - self.indices = [] - self.challenge_conf = os.path.join( - configurator.config.config_dir, "le_dvsni_cert_challenge.conf") - # self.completed = 0 - - def add_chall(self, achall, idx=None): - """Add challenge to DVSNI object to perform at once. - - :param achall: Annotated DVSNI challenge. - :type achall: :class:`letsencrypt.client.achallenges.DVSNI` - - :param int idx: index to challenge in a larger array - - """ - self.achalls.append(achall) - if idx is not None: - self.indices.append(idx) - def perform(self): - """Peform a DVSNI challenge.""" + """Perform a DVSNI challenge on Nginx.""" if not self.achalls: return [] - # Save any changes to the configuration as a precaution - # About to make temporary changes to the config + self.configurator.save() addresses = [] - default_addr = "*:443" + # default_addr = "*:443" for achall in self.achalls: vhost = self.configurator.choose_vhost(achall.domain) if vhost is None: logging.error( - "No vhost exists with servername or alias of: %s", + "No nginx vhost exists with servername or alias of: %s", achall.domain) - logging.error("No _default_:443 vhost exists") + logging.error("No default 443 nginx vhost exists") logging.error("Please specify servernames in the Nginx config") return None - for addr in vhost.addrs: - if "_default_" == addr.get_addr(): - addresses.append([default_addr]) - break + # for addr in vhost.addrs: + # if "_default_" == addr.get_addr(): + # addresses.append([default_addr]) + # break else: addresses.append(list(vhost.addrs)) diff --git a/letsencrypt/client/plugins/nginx/obj.py b/letsencrypt/client/plugins/nginx/obj.py index 3509c16f9..acaacb3b0 100644 --- a/letsencrypt/client/plugins/nginx/obj.py +++ b/letsencrypt/client/plugins/nginx/obj.py @@ -1,8 +1,10 @@ """Module contains classes used by the Nginx Configurator.""" import re +from letsencrypt.client.plugins.apache.obj import Addr as ApacheAddr -class Addr(object): + +class Addr(ApacheAddr): """Represents an Nginx address, i.e. what comes after the 'listen' directive. @@ -24,7 +26,7 @@ class Addr(object): """ def __init__(self, host, port, ssl, default): - self.tup = (host, port) + super(Addr, self).__init__((host, port)) self.ssl = ssl self.default = default @@ -79,17 +81,6 @@ class Addr(object): self.default == other.default) return False - def __hash__(self): - return hash(self.tup) - - def get_addr(self): - """Return addr part of Addr object.""" - return self.tup[0] - - def get_port(self): - """Return port.""" - return self.tup[1] - class VirtualHost(object): # pylint: disable=too-few-public-methods """Represents an Nginx Virtualhost. diff --git a/letsencrypt/client/plugins/nginx/parser.py b/letsencrypt/client/plugins/nginx/parser.py index dca022022..55a0b01e8 100644 --- a/letsencrypt/client/plugins/nginx/parser.py +++ b/letsencrypt/client/plugins/nginx/parser.py @@ -178,10 +178,10 @@ class NginxParser(object): root = self._find_config_root() default = root - temp = os.path.join(self.root, "ports.conf") - if os.path.isfile(temp): - listen = temp - name = temp + nginx_temp = os.path.join(self.root, "nginx_ports.conf") + if os.path.isfile(nginx_temp): + listen = nginx_temp + name = nginx_temp else: listen = default name = default diff --git a/letsencrypt/client/plugins/nginx/tests/configurator_test.py b/letsencrypt/client/plugins/nginx/tests/configurator_test.py index 35c2573ef..225ab1610 100644 --- a/letsencrypt/client/plugins/nginx/tests/configurator_test.py +++ b/letsencrypt/client/plugins/nginx/tests/configurator_test.py @@ -167,18 +167,18 @@ class NginxConfiguratorTest(util.NginxTest): auth_key = le_util.Key(self.rsa256_file, self.rsa256_pem) achall1 = achallenges.DVSNI( chall=challenges.DVSNI( - r="jIq_Xy1mXGN37tb4L6Xj_es58fW571ZNyXekdZzhh7Q", - nonce="37bc5eb75d3e00a19b4f6355845e5a18"), - domain="encryption-example.demo", key=auth_key) + r="foo", + nonce="bar"), + domain="localhost", key=auth_key) achall2 = achallenges.DVSNI( chall=challenges.DVSNI( - r="uqnaPzxtrndteOqtrXb0Asl5gOJfWAnnx6QJyvcmlDU", - nonce="59ed014cac95f77057b1d7a1b2c596ba"), - domain="letsencrypt.demo", key=auth_key) + r="abc", + nonce="def"), + domain="example.com", key=auth_key) dvsni_ret_val = [ - challenges.DVSNIResponse(s="randomS1"), - challenges.DVSNIResponse(s="randomS2"), + challenges.DVSNIResponse(s="irrelevant"), + challenges.DVSNIResponse(s="arbitrary"), ] mock_dvsni_perform.return_value = dvsni_ret_val diff --git a/letsencrypt/client/plugins/nginx/tests/dvsni_test.py b/letsencrypt/client/plugins/nginx/tests/dvsni_test.py index 98fefebe1..a6dfac2e2 100644 --- a/letsencrypt/client/plugins/nginx/tests/dvsni_test.py +++ b/letsencrypt/client/plugins/nginx/tests/dvsni_test.py @@ -23,21 +23,21 @@ class DvsniPerformTest(util.NginxTest): self.config_path, self.config_dir, self.work_dir, self.ssl_options) - from letsencrypt.client.plugins.nginx import dvsni - self.sni = dvsni.NginxDvsni(config) - rsa256_file = pkg_resources.resource_filename( "letsencrypt.client.tests", "testdata/rsa256_key.pem") rsa256_pem = pkg_resources.resource_string( "letsencrypt.client.tests", "testdata/rsa256_key.pem") auth_key = le_util.Key(rsa256_file, rsa256_pem) + + from letsencrypt.client.plugins.nginx import dvsni + self.sni = dvsni.NginxDvsni(config) + self.achalls = [ achallenges.DVSNI( chall=challenges.DVSNI( - r="\x8c\x8a\xbf_-f\\cw\xee\xd6\xf8/\xa5\xe3\xfd\xeb9\xf1" - "\xf5\xb9\xefVM\xc9w\xa4u\x9c\xe1\x87\xb4", - nonce="7\xbc^\xb7]>\x00\xa1\x9bOcU\x84^Z\x18", + r="foo", + nonce="bar", ), domain="www.example.com", key=auth_key), achallenges.DVSNI( chall=challenges.DVSNI( diff --git a/letsencrypt/client/plugins/nginx/tests/util.py b/letsencrypt/client/plugins/nginx/tests/util.py index 4a4502379..4570f2de2 100644 --- a/letsencrypt/client/plugins/nginx/tests/util.py +++ b/letsencrypt/client/plugins/nginx/tests/util.py @@ -66,16 +66,11 @@ def get_nginx_configurator( config = configurator.NginxConfigurator( mock.MagicMock( - nginx_server_root=config_path, - nginx_mod_ssl_conf=ssl_options, - le_vhost_ext="-le-ssl.conf", - backup_dir=backups, - config_dir=config_dir, + nginx_server_root=config_path, nginx_mod_ssl_conf=ssl_options, + le_vhost_ext="-le-ssl.conf", backup_dir=backups, + config_dir=config_dir, work_dir=work_dir, temp_checkpoint_dir=os.path.join(work_dir, "temp_checkpoints"), - in_progress_dir=os.path.join(backups, "IN_PROGRESS"), - work_dir=work_dir), + in_progress_dir=os.path.join(backups, "IN_PROGRESS")), version) - config.prepare() - return config From c67f1c11b417ed2471f5beb2ff507dc371fdf3c0 Mon Sep 17 00:00:00 2001 From: yan Date: Fri, 17 Apr 2015 18:23:24 -0700 Subject: [PATCH 34/38] Update LICENSE.txt for nginxparser attribution --- LICENSE.txt | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/LICENSE.txt b/LICENSE.txt index 67db85882..d3c19bbd1 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,4 +1,14 @@ +Let's Encrypt Preview: +Copyright (c) Internet Security Research Group +Licensed Apache Version 2.0 +Incorporating code from nginxparser +Copyright (c) 2014 Fatih Erikli +Licensed MIT + + +Text of Apache License +====================== Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ @@ -173,3 +183,23 @@ defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + + +Text of MIT License +=================== +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. From 636f5aa313a617c296251886eb3d58bf4dc32657 Mon Sep 17 00:00:00 2001 From: yan Date: Fri, 17 Apr 2015 23:21:55 -0700 Subject: [PATCH 35/38] Remove commented-out code in nginx dvsni.py --- letsencrypt/client/plugins/nginx/dvsni.py | 110 ---------------------- 1 file changed, 110 deletions(-) diff --git a/letsencrypt/client/plugins/nginx/dvsni.py b/letsencrypt/client/plugins/nginx/dvsni.py index 9535a90c7..7233d7c62 100644 --- a/letsencrypt/client/plugins/nginx/dvsni.py +++ b/letsencrypt/client/plugins/nginx/dvsni.py @@ -28,17 +28,6 @@ class NginxDvsni(ApacheDvsni): """ - VHOST_TEMPLATE = """\ - - ServerName {server_name} - Include {ssl_options_conf_path} - SSLCertificateFile {cert_path} - SSLCertificateKeyFile {key_path} - DocumentRoot {document_root} - - -""" - def perform(self): """Perform a DVSNI challenge on Nginx.""" if not self.achalls: @@ -47,7 +36,6 @@ class NginxDvsni(ApacheDvsni): self.configurator.save() addresses = [] - # default_addr = "*:443" for achall in self.achalls: vhost = self.configurator.choose_vhost(achall.domain) if vhost is None: @@ -57,11 +45,6 @@ class NginxDvsni(ApacheDvsni): logging.error("No default 443 nginx vhost exists") logging.error("Please specify servernames in the Nginx config") return None - - # for addr in vhost.addrs: - # if "_default_" == addr.get_addr(): - # addresses.append([default_addr]) - # break else: addresses.append(list(vhost.addrs)) @@ -78,96 +61,3 @@ class NginxDvsni(ApacheDvsni): self.configurator.save("SNI Challenge", True) return responses - -# def _setup_challenge_cert(self, achall, s=None): -# # pylint: disable=invalid-name -# """Generate and write out challenge certificate.""" -# cert_path = self.get_cert_file(achall) -# # Register the path before you write out the file -# self.configurator.reverter.register_file_creation(True, cert_path) -# -# cert_pem, response = achall.gen_cert_and_response(s) -# -# # Write out challenge cert -# with open(cert_path, "w") as cert_chall_fd: -# cert_chall_fd.write(cert_pem) -# -# return response -# -# def _mod_config(self, ll_addrs): -# """Modifies Nginx config files to include challenge vhosts. -# -# Result: Nginx config includes virtual servers for issued challs -# -# :param list ll_addrs: list of list of -# :class:`letsencrypt.client.plugins.nginx.obj.Addr` to apply -# -# """ -# # TODO: Use ip address of existing vhost instead of relying on FQDN -# config_text = "\n" -# for idx, lis in enumerate(ll_addrs): -# config_text += self._get_config_text(self.achalls[idx], lis) -# config_text += "\n" -# -# self._conf_include_check(self.configurator.parser.loc["default"]) -# self.configurator.reverter.register_file_creation( -# True, self.challenge_conf) -# -# with open(self.challenge_conf, "w") as new_conf: -# new_conf.write(config_text) -# -# def _conf_include_check(self, main_config): -# """Adds DVSNI challenge conf file into configuration. -# -# Adds DVSNI challenge include file if it does not already exist -# within mainConfig -# -# :param str main_config: file path to main user nginx 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_config_text(self, achall, ip_addrs): -# """Chocolate virtual server configuration text -# -# :param achall: Annotated DVSNI challenge. -# :type achall: :class:`letsencrypt.client.achallenges.DVSNI` -# -# :param list ip_addrs: addresses of challenged domain -# :class:`list` of type :class:`~nginx.obj.Addr` -# -# :returns: virtual host configuration text -# :rtype: str -# -# """ -# ips = " ".join(str(i) for i in ip_addrs) -# document_root = os.path.join( -# self.configurator.config.config_dir, "dvsni_page/") -# # TODO: Python docs is not clear how mutliline string literal -# # newlines are parsed on different platforms. At least on -# # Linux (Debian sid), when source file uses CRLF, Python still -# # parses it as "\n"... c.f.: -# # https://docs.python.org/2.7/reference/lexical_analysis.html -# return self.VHOST_TEMPLATE.format( -# vhost=ips, server_name=achall.nonce_domain, -# ssl_options_conf_path=self.configurator.parser.loc["ssl_options"], -# cert_path=self.get_cert_file(achall), key_path=achall.key.file, -# document_root=document_root).replace("\n", os.linesep) -# -# def get_cert_file(self, achall): -# """Returns standardized name for challenge certificate. -# -# :param achall: Annotated DVSNI challenge. -# :type achall: :class:`letsencrypt.client.achallenges.DVSNI` -# -# :returns: certificate file name -# :rtype: str -# -# """ -# return os.path.join( -# self.configurator.config.work_dir, achall.nonce_domain + ".crt") From 4bcc18d9d35393eb454f131eef434d9e67af1456 Mon Sep 17 00:00:00 2001 From: yan Date: Sat, 18 Apr 2015 10:20:19 -0700 Subject: [PATCH 36/38] Address @kuba's review comments --- .../client/plugins/nginx/configurator.py | 30 ++-- .../client/plugins/nginx/nginxparser.py | 24 +-- .../plugins/nginx/tests/nginxparser_test.py | 22 +-- .../client/plugins/nginx/tests/parser_test.py | 154 +++++++++--------- 4 files changed, 113 insertions(+), 117 deletions(-) diff --git a/letsencrypt/client/plugins/nginx/configurator.py b/letsencrypt/client/plugins/nginx/configurator.py index d799432f3..47a732070 100644 --- a/letsencrypt/client/plugins/nginx/configurator.py +++ b/letsencrypt/client/plugins/nginx/configurator.py @@ -90,11 +90,14 @@ class NginxConfigurator(object): # Entry point in main.py for installing cert def deploy_cert(self, domain, cert, key, cert_chain=None): # pylint: disable=unused-argument - """Deploys certificate to specified virtual host. Aborts if the - vhost is missing ssl_certificate or ssl_certificate_key. + """Deploys certificate to specified virtual host. - Nginx doesn't have a cert chain directive, so the last parameter is - always ignored. It expects the cert file to have the concatenated chain. + .. note:: Aborts if the vhost is missing ssl_certificate or + ssl_certificate_key. + + .. note:: Nginx doesn't have a cert chain directive, so the last + parameter is always ignored. It expects the cert file to have + the concatenated chain. .. note:: This doesn't save the config files! @@ -130,9 +133,11 @@ class NginxConfigurator(object): # Vhost parsing methods ####################### def choose_vhost(self, target_name): - """Chooses a virtual host based on the given domain name. NOTE: This - makes the vhost SSL-enabled if it isn't already. Follows Nginx's server - block selection rules but prefers blocks that are already SSL. + """Chooses a virtual host based on the given domain name. + + .. note:: This makes the vhost SSL-enabled if it isn't already. Follows + Nginx's server block selection rules preferring blocks that are + already SSL. .. todo:: This should maybe return list if no obvious answer is presented. @@ -149,10 +154,10 @@ class NginxConfigurator(object): vhost = None matches = self._get_ranked_matches(target_name) - if len(matches) == 0: + if not matches: # No matches at all :'( pass - elif matches[0]['rank'] in range(2, 6): + elif matches[0]['rank'] in xrange(2, 6): # Wildcard match - need to find the longest one rank = matches[0]['rank'] wildcards = [x for x in matches if x['rank'] == rank] @@ -167,8 +172,7 @@ class NginxConfigurator(object): return vhost def _get_ranked_matches(self, target_name): - """ - Returns a ranked list of vhosts that match target_name. + """Returns a ranked list of vhosts that match target_name. :param str target_name: The name to match :returns: list of dicts containing the vhost, the matching name, and @@ -374,10 +378,10 @@ class NginxConfigurator(object): sni_regex = re.compile(r"TLS SNI support enabled", re.IGNORECASE) sni_matches = sni_regex.findall(text) - if len(version_matches) == 0: + if not version_matches: raise errors.LetsEncryptConfiguratorError( "Unable to find Nginx version") - if len(sni_matches) == 0: + if not sni_matches: raise errors.LetsEncryptConfiguratorError( "Nginx build doesn't support SNI") diff --git a/letsencrypt/client/plugins/nginx/nginxparser.py b/letsencrypt/client/plugins/nginx/nginxparser.py index 947c05f2e..18ba8b0bd 100644 --- a/letsencrypt/client/plugins/nginx/nginxparser.py +++ b/letsencrypt/client/plugins/nginx/nginxparser.py @@ -8,9 +8,7 @@ from pyparsing import ( class RawNginxParser(object): # pylint: disable=expression-not-assigned - """ - A class that parses nginx configuration with pyparsing - """ + """A class that parses nginx configuration with pyparsing.""" # constants left_bracket = Literal("{").suppress() @@ -39,31 +37,23 @@ class RawNginxParser(object): self.source = source def parse(self): - """ - Returns the parsed tree. - """ + """Returns the parsed tree.""" return self.script.parseString(self.source) def as_list(self): - """ - Returns the list of tree. - """ + """Returns the parsed tree as a list.""" return self.parse().asList() class RawNginxDumper(object): # pylint: disable=too-few-public-methods - """ - A class that dumps nginx configuration from the provided tree. - """ + """A class that dumps nginx configuration from the provided tree.""" def __init__(self, blocks, indentation=4): self.blocks = blocks self.indentation = indentation def __iter__(self, blocks=None, current_indent=0, spacer=' '): - """ - Iterates the dumped nginx content. - """ + """Iterates the dumped nginx content.""" blocks = blocks or self.blocks for key, values in blocks: if current_indent: @@ -88,9 +78,7 @@ class RawNginxDumper(object): yield spacer * current_indent + key + spacer + values + ';' def as_string(self): - """ - Return the parsed block as a string. - """ + """Return the parsed block as a string.""" return '\n'.join(self) diff --git a/letsencrypt/client/plugins/nginx/tests/nginxparser_test.py b/letsencrypt/client/plugins/nginx/tests/nginxparser_test.py index b249b25cc..2e19e71d1 100644 --- a/letsencrypt/client/plugins/nginx/tests/nginxparser_test.py +++ b/letsencrypt/client/plugins/nginx/tests/nginxparser_test.py @@ -48,17 +48,17 @@ class TestRawNginxParser(unittest.TestCase): ]]]) self.assertEqual(dumped, - 'user www-data;\n' + - 'server {\n' + - ' listen 80;\n' + - ' server_name foo.com;\n' + - ' root /home/ubuntu/sites/foo/;\n \n' + - ' location /status {\n' + - ' check_status;\n \n' + - ' types {\n' + - ' image/jpeg jpg;\n' + - ' }\n' + - ' }\n' + + 'user www-data;\n' + 'server {\n' + ' listen 80;\n' + ' server_name foo.com;\n' + ' root /home/ubuntu/sites/foo/;\n \n' + ' location /status {\n' + ' check_status;\n \n' + ' types {\n' + ' image/jpeg jpg;\n' + ' }\n' + ' }\n' '}') def test_parse_from_file(self): diff --git a/letsencrypt/client/plugins/nginx/tests/parser_test.py b/letsencrypt/client/plugins/nginx/tests/parser_test.py index a76f2da25..21e96aa26 100644 --- a/letsencrypt/client/plugins/nginx/tests/parser_test.py +++ b/letsencrypt/client/plugins/nginx/tests/parser_test.py @@ -6,9 +6,9 @@ import shutil import unittest from letsencrypt.client.errors import LetsEncryptMisconfigurationError -from letsencrypt.client.plugins.nginx.nginxparser import dumps -from letsencrypt.client.plugins.nginx.obj import Addr, VirtualHost -from letsencrypt.client.plugins.nginx.parser import NginxParser, get_best_match +from letsencrypt.client.plugins.nginx import nginxparser +from letsencrypt.client.plugins.nginx import obj +from letsencrypt.client.plugins.nginx import parser from letsencrypt.client.plugins.nginx.tests import util @@ -26,52 +26,52 @@ class NginxParserTest(util.NginxTest): def test_root_normalized(self): path = os.path.join(self.temp_dir, "foo/////" "bar/../../testdata") - parser = NginxParser(path, None) - self.assertEqual(parser.root, self.config_path) + nparser = parser.NginxParser(path, None) + self.assertEqual(nparser.root, self.config_path) def test_root_absolute(self): - parser = NginxParser(os.path.relpath(self.config_path), None) - self.assertEqual(parser.root, self.config_path) + nparser = parser.NginxParser(os.path.relpath(self.config_path), None) + self.assertEqual(nparser.root, self.config_path) def test_root_no_trailing_slash(self): - parser = NginxParser(self.config_path + os.path.sep, None) - self.assertEqual(parser.root, self.config_path) + nparser = parser.NginxParser(self.config_path + os.path.sep, None) + self.assertEqual(nparser.root, self.config_path) def test_load(self): """Test recursive conf file parsing. """ - parser = NginxParser(self.config_path, self.ssl_options) - parser.load() - self.assertEqual(set([parser.abs_path(x) for x in + nparser = parser.NginxParser(self.config_path, self.ssl_options) + nparser.load() + self.assertEqual(set([nparser.abs_path(x) for x in ['foo.conf', 'nginx.conf', 'server.conf', 'sites-enabled/default', 'sites-enabled/example.com']]), - set(parser.parsed.keys())) + set(nparser.parsed.keys())) self.assertEqual([['server_name', 'somename alias another.alias']], - parser.parsed[parser.abs_path('server.conf')]) + nparser.parsed[nparser.abs_path('server.conf')]) self.assertEqual([[['server'], [['listen', '69.50.225.155:9000'], ['listen', '127.0.0.1'], ['server_name', '.example.com'], ['server_name', 'example.*']]]], - parser.parsed[parser.abs_path( + nparser.parsed[nparser.abs_path( 'sites-enabled/example.com')]) def test_abs_path(self): - parser = NginxParser(self.config_path, self.ssl_options) - self.assertEqual('/etc/nginx/*', parser.abs_path('/etc/nginx/*')) + nparser = parser.NginxParser(self.config_path, self.ssl_options) + self.assertEqual('/etc/nginx/*', nparser.abs_path('/etc/nginx/*')) self.assertEqual(os.path.join(self.config_path, 'foo/bar/'), - parser.abs_path('foo/bar/')) + nparser.abs_path('foo/bar/')) def test_filedump(self): - parser = NginxParser(self.config_path, self.ssl_options) - parser.filedump('test') + nparser = parser.NginxParser(self.config_path, self.ssl_options) + nparser.filedump('test') # pylint: disable=protected-access - parsed = parser._parse_files(parser.abs_path( + parsed = nparser._parse_files(nparser.abs_path( 'sites-enabled/example.com.test')) - self.assertEqual(3, len(glob.glob(parser.abs_path('*.test')))) + self.assertEqual(3, len(glob.glob(nparser.abs_path('*.test')))) self.assertEqual(2, len( - glob.glob(parser.abs_path('sites-enabled/*.test')))) + glob.glob(nparser.abs_path('sites-enabled/*.test')))) self.assertEqual([[['server'], [['listen', '69.50.225.155:9000'], ['listen', '127.0.0.1'], ['server_name', '.example.com'], @@ -79,31 +79,34 @@ class NginxParserTest(util.NginxTest): parsed[0]) def test_get_vhosts(self): - parser = NginxParser(self.config_path, self.ssl_options) - vhosts = parser.get_vhosts() + nparser = parser.NginxParser(self.config_path, self.ssl_options) + vhosts = nparser.get_vhosts() - vhost1 = VirtualHost(parser.abs_path('nginx.conf'), - [Addr('', '8080', False, False)], - False, True, set(['localhost', - r'~^(www\.)?(example|bar)\.']), - []) - vhost2 = VirtualHost(parser.abs_path('nginx.conf'), - [Addr('somename', '8080', False, False), - Addr('', '8000', False, False)], - False, True, set(['somename', - 'another.alias', 'alias']), []) - vhost3 = VirtualHost(parser.abs_path('sites-enabled/example.com'), - [Addr('69.50.225.155', '9000', False, False), - Addr('127.0.0.1', '', False, False)], - False, True, set(['.example.com', 'example.*']), - []) - vhost4 = VirtualHost(parser.abs_path('sites-enabled/default'), - [Addr('myhost', '', False, True)], - False, True, set(['www.example.org']), []) - vhost5 = VirtualHost(parser.abs_path('foo.conf'), - [Addr('*', '80', True, True)], - True, True, set(['*.www.foo.com', - '*.www.example.com']), []) + vhost1 = obj.VirtualHost(nparser.abs_path('nginx.conf'), + [obj.Addr('', '8080', False, False)], + False, True, + set(['localhost', + r'~^(www\.)?(example|bar)\.']), + []) + vhost2 = obj.VirtualHost(nparser.abs_path('nginx.conf'), + [obj.Addr('somename', '8080', False, False), + obj.Addr('', '8000', False, False)], + False, True, + set(['somename', 'another.alias', 'alias']), + []) + vhost3 = obj.VirtualHost(nparser.abs_path('sites-enabled/example.com'), + [obj.Addr('69.50.225.155', '9000', + False, False), + obj.Addr('127.0.0.1', '', False, False)], + False, True, + set(['.example.com', 'example.*']), []) + vhost4 = obj.VirtualHost(nparser.abs_path('sites-enabled/default'), + [obj.Addr('myhost', '', False, True)], + False, True, set(['www.example.org']), []) + vhost5 = obj.VirtualHost(nparser.abs_path('foo.conf'), + [obj.Addr('*', '80', True, True)], + True, True, set(['*.www.foo.com', + '*.www.example.com']), []) self.assertEqual(5, len(vhosts)) example_com = [x for x in vhosts if 'example.com' in x.filep][0] @@ -118,39 +121,39 @@ class NginxParserTest(util.NginxTest): self.assertEquals(vhost2, somename) def test_add_server_directives(self): - parser = NginxParser(self.config_path, self.ssl_options) - parser.add_server_directives(parser.abs_path('nginx.conf'), - set(['localhost', - r'~^(www\.)?(example|bar)\.']), - [['foo', 'bar'], ['ssl_certificate', - '/etc/ssl/cert.pem']]) + nparser = parser.NginxParser(self.config_path, self.ssl_options) + nparser.add_server_directives(nparser.abs_path('nginx.conf'), + set(['localhost', + r'~^(www\.)?(example|bar)\.']), + [['foo', 'bar'], ['ssl_certificate', + '/etc/ssl/cert.pem']]) ssl_re = re.compile(r'foo bar;\n\s+ssl_certificate /etc/ssl/cert.pem') - self.assertEqual(1, len(re.findall(ssl_re, dumps(parser.parsed[ - parser.abs_path('nginx.conf')])))) - parser.add_server_directives(parser.abs_path('server.conf'), - set(['alias', 'another.alias', - 'somename']), - [['foo', 'bar'], ['ssl_certificate', - '/etc/ssl/cert2.pem']]) - self.assertEqual(parser.parsed[parser.abs_path('server.conf')], + self.assertEqual(1, len(re.findall(ssl_re, nginxparser.dumps( + nparser.parsed[nparser.abs_path('nginx.conf')])))) + nparser.add_server_directives(nparser.abs_path('server.conf'), + set(['alias', 'another.alias', + 'somename']), + [['foo', 'bar'], ['ssl_certificate', + '/etc/ssl/cert2.pem']]) + self.assertEqual(nparser.parsed[nparser.abs_path('server.conf')], [['server_name', 'somename alias another.alias'], ['foo', 'bar'], ['ssl_certificate', '/etc/ssl/cert2.pem']]) def test_replace_server_directives(self): - parser = NginxParser(self.config_path, self.ssl_options) + nparser = parser.NginxParser(self.config_path, self.ssl_options) target = set(['.example.com', 'example.*']) - filep = parser.abs_path('sites-enabled/example.com') - parser.add_server_directives( + filep = nparser.abs_path('sites-enabled/example.com') + nparser.add_server_directives( filep, target, [['server_name', 'foo bar']], True) self.assertEqual( - parser.parsed[filep], + nparser.parsed[filep], [[['server'], [['listen', '69.50.225.155:9000'], ['listen', '127.0.0.1'], ['server_name', 'foo bar'], ['server_name', 'foo bar']]]]) self.assertRaises(LetsEncryptMisconfigurationError, - parser.add_server_directives, + nparser.add_server_directives, filep, set(['foo', 'bar']), [['ssl_certificate', 'cert.pem']], True) @@ -184,17 +187,18 @@ class NginxParserTest(util.NginxTest): (None, None)] for i, winner in enumerate(winners): - self.assertEqual(winner, get_best_match(target_name, names[i])) + self.assertEqual(winner, + parser.get_best_match(target_name, names[i])) def test_get_all_certs_keys(self): - parser = NginxParser(self.config_path, self.ssl_options) - filep = parser.abs_path('sites-enabled/example.com') - parser.add_server_directives(filep, - set(['.example.com', 'example.*']), - [['ssl_certificate', 'foo.pem'], - ['ssl_certificate_key', 'bar.key'], - ['listen', '443 ssl']]) - c_k = parser.get_all_certs_keys() + nparser = parser.NginxParser(self.config_path, self.ssl_options) + filep = nparser.abs_path('sites-enabled/example.com') + nparser.add_server_directives(filep, + set(['.example.com', 'example.*']), + [['ssl_certificate', 'foo.pem'], + ['ssl_certificate_key', 'bar.key'], + ['listen', '443 ssl']]) + c_k = nparser.get_all_certs_keys() self.assertEqual(set([('foo.pem', 'bar.key', filep)]), c_k) From 18582e8ca0c3a37f013484dee801857715fdc82a Mon Sep 17 00:00:00 2001 From: yan Date: Mon, 20 Apr 2015 10:58:02 -0700 Subject: [PATCH 37/38] Fix tuple comparison, add ssl check in nginx get_version --- .../client/plugins/nginx/configurator.py | 10 ++++--- .../plugins/nginx/tests/configurator_test.py | 26 +++++++++++++++++-- 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/letsencrypt/client/plugins/nginx/configurator.py b/letsencrypt/client/plugins/nginx/configurator.py index 47a732070..84588ffe8 100644 --- a/letsencrypt/client/plugins/nginx/configurator.py +++ b/letsencrypt/client/plugins/nginx/configurator.py @@ -378,9 +378,15 @@ class NginxConfigurator(object): sni_regex = re.compile(r"TLS SNI support enabled", re.IGNORECASE) sni_matches = sni_regex.findall(text) + ssl_regex = re.compile(r" --with-http_ssl_module") + ssl_matches = ssl_regex.findall(text) + if not version_matches: raise errors.LetsEncryptConfiguratorError( "Unable to find Nginx version") + if not ssl_matches: + raise errors.LetsEncryptConfiguratorError( + "Nginx build is missing SSL module (--with-http_ssl_module).") if not sni_matches: raise errors.LetsEncryptConfiguratorError( "Nginx build doesn't support SNI") @@ -388,9 +394,7 @@ class NginxConfigurator(object): nginx_version = tuple([int(i) for i in version_matches[0].split(".")]) # nginx < 0.8.21 doesn't use default_server - if (nginx_version[0] == 0 and (nginx_version[1] < 8 or - (nginx_version[1] == 8 and - nginx_version[2] < 21))): + if nginx_version < (0, 8, 21): raise errors.LetsEncryptConfiguratorError( "Nginx version must be 0.8.21+") diff --git a/letsencrypt/client/plugins/nginx/tests/configurator_test.py b/letsencrypt/client/plugins/nginx/tests/configurator_test.py index 225ab1610..0ac0fd8bc 100644 --- a/letsencrypt/client/plugins/nginx/tests/configurator_test.py +++ b/letsencrypt/client/plugins/nginx/tests/configurator_test.py @@ -200,21 +200,43 @@ class NginxConfiguratorTest(util.NginxTest): "nginx/1.6.2 --with-http_ssl_module"])) self.assertEqual(self.config.get_version(), (1, 4, 2)) + mock_popen().communicate.return_value = ( + "", "\n".join(["nginx version: nginx/0.9", + "built by clang 6.0 (clang-600.0.56)" + " (based on LLVM 3.5svn)", + "TLS SNI support enabled", + "configure arguments: --with-http_ssl_module"])) + self.assertEqual(self.config.get_version(), (0, 9)) + mock_popen().communicate.return_value = ( "", "\n".join(["blah 0.0.1", + "built by clang 6.0 (clang-600.0.56)" + " (based on LLVM 3.5svn)", + "TLS SNI support enabled", + "configure arguments: --with-http_ssl_module"])) + self.assertRaises(errors.LetsEncryptConfiguratorError, + self.config.get_version) + + mock_popen().communicate.return_value = ( + "", "\n".join(["nginx version: nginx/1.4.2", "TLS SNI support enabled"])) self.assertRaises(errors.LetsEncryptConfiguratorError, self.config.get_version) mock_popen().communicate.return_value = ( "", "\n".join(["nginx version: nginx/1.4.2", - ""])) + "built by clang 6.0 (clang-600.0.56)" + " (based on LLVM 3.5svn)", + "configure arguments: --with-http_ssl_module"])) self.assertRaises(errors.LetsEncryptConfiguratorError, self.config.get_version) mock_popen().communicate.return_value = ( "", "\n".join(["nginx version: nginx/0.8.1", - ""])) + "built by clang 6.0 (clang-600.0.56)" + " (based on LLVM 3.5svn)", + "TLS SNI support enabled", + "configure arguments: --with-http_ssl_module"])) self.assertRaises(errors.LetsEncryptConfiguratorError, self.config.get_version) From 6a0dc2b9608ee539d22f1b495074586098669168 Mon Sep 17 00:00:00 2001 From: yan Date: Mon, 20 Apr 2015 11:03:27 -0700 Subject: [PATCH 38/38] Improve comments based on PR #351 review --- letsencrypt/client/plugins/nginx/configurator.py | 3 ++- setup.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/letsencrypt/client/plugins/nginx/configurator.py b/letsencrypt/client/plugins/nginx/configurator.py index 84588ffe8..ebafe8286 100644 --- a/letsencrypt/client/plugins/nginx/configurator.py +++ b/letsencrypt/client/plugins/nginx/configurator.py @@ -26,7 +26,8 @@ class NginxConfigurator(object): # pylint: disable=too-many-instance-attributes,too-many-public-methods """Nginx configurator. - .. todo:: Add proper support for comments in the config + .. todo:: Add proper support for comments in the config. Currently, + config files modified by the configurator will lose all their comments. :ivar config: Configuration. :type config: :class:`~letsencrypt.client.interfaces.IConfig` diff --git a/setup.py b/setup.py index 258992bae..a4c7f7683 100644 --- a/setup.py +++ b/setup.py @@ -37,7 +37,7 @@ install_requires = [ 'pyasn1', # urllib3 InsecurePlatformWarning (#304) 'pycrypto', 'PyOpenSSL', - 'pyparsing>=1.5.5', + 'pyparsing>=1.5.5', # Python3 support; perhaps unnecessary? 'pyrfc3339', 'python-augeas', 'python2-pythondialog>=3.2.2rc1', # Debian squeeze support, cf. #280