Nginx versioning and other config changes

This commit is contained in:
yan
2015-04-16 13:47:26 -07:00
parent 46db59d774
commit 37649966c2
5 changed files with 58 additions and 200 deletions
+6
View File
@@ -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."""
+8
View File
@@ -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.
+35 -200
View File
@@ -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. <VirtualHost>, <IfModule> 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 <VirtualHost> </Virtualhost> 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)
+7
View File
@@ -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
+2
View File
@@ -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',
],