From 37649966c20f6aeab2c44ad74e24a315a80347d4 Mon Sep 17 00:00:00 2001 From: yan Date: Thu, 2 Apr 2015 18:15:17 -0700 Subject: [PATCH] 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', ],