Merge branch 'master' into pyopenssl

This commit is contained in:
Brad Warren
2016-01-06 12:40:56 -05:00
61 changed files with 600 additions and 258 deletions
+11
View File
@@ -3,6 +3,8 @@ language: python
services:
- rabbitmq
- mariadb
# apacheconftest
#- apache2
# http://docs.travis-ci.com/user/ci-environment/#CI-environment-OS
# gimme has to be kept in sync with Boulder's Go version setting in .travis.yml
@@ -22,6 +24,9 @@ env:
- TOXENV=py27 BOULDER_INTEGRATION=1
- TOXENV=lint
- TOXENV=cover
# Disabled for now due to requiring sudo -> causing more boulder integration
# DNS timeouts :(
# - TOXENV=apacheconftest
# Only build pushes to the master branch, PRs, and branches beginning with
@@ -58,6 +63,12 @@ addons:
- openssl
# For Boulder integration testing
- rsyslog
# for apacheconftest
#- realpath
#- apache2
#- libapache2-mod-wsgi
#- libapache2-mod-macro
#- sudo
install: "travis_retry pip install tox coveralls"
script: 'travis_retry tox && ([ "xxx$BOULDER_INTEGRATION" = "xxx" ] || ./tests/travis-integration.sh)'
+2 -2
View File
@@ -3,10 +3,10 @@
This module is an implementation of the `ACME protocol`_. Latest
supported version: `draft-ietf-acme-01`_.
.. _`ACME protocol`: https://github.com/ietf-wg-acme/acme/
.. _`ACME protocol`: https://ietf-wg-acme.github.io/acme
.. _`draft-ietf-acme-01`:
https://github.com/ietf-wg-acme/acme/tree/draft-ietf-acme-acme-01
"""
+1 -1
View File
@@ -226,7 +226,7 @@ class JSONObjectWithFields(util.ImmutableMap, interfaces.JSONDeSerializable):
:param str name: Name of the field to be encoded.
:raises erors.SerializationError: if field cannot be serialized
:raises errors.SerializationError: if field cannot be serialized
:raises errors.Error: if field could not be found
"""
+1 -1
View File
@@ -139,7 +139,7 @@ class ImmutableMap(collections.Mapping, collections.Hashable):
"""Immutable key to value mapping with attribute access."""
__slots__ = ()
"""Must be overriden in subclasses."""
"""Must be overridden in subclasses."""
def __init__(self, **kwargs):
if set(kwargs) != set(self.__slots__):
+1 -1
View File
@@ -31,7 +31,7 @@ else:
install_requires.append('mock')
if sys.version_info < (2, 7, 9):
# For secure SSL connexion with Python 2.7 (InsecurePlatformWarning)
# For secure SSL connection with Python 2.7 (InsecurePlatformWarning)
install_requires.append('ndg-httpsclient')
install_requires.append('pyasn1')
+8
View File
@@ -45,3 +45,11 @@ then
echo "Could not install additional dependencies. Aborting bootstrap!"
exit 1
fi
if $tool list installed "httpd" >/dev/null 2>&1; then
if ! $tool install -y mod_ssl
then
echo "Apache found, but mod_ssl could not be installed."
fi
fi
+8 -2
View File
@@ -65,8 +65,14 @@ Testing
The following tools are there to help you:
- ``tox`` starts a full set of tests. Please make sure you run it
before submitting a new pull request.
- ``tox`` starts a full set of tests. Please note that it includes
apacheconftest, which uses the system's Apache install to test config file
parsing, so it should only be run on systems that have an
experimental, non-production Apache2 install on them. ``tox -e
apacheconftest`` can be used to run those specific Apache conf tests.
- ``tox -e py27``, ``tox -e py26`` etc, run unit tests for specific Python
versions.
- ``tox -e cover`` checks the test coverage only. Calling the
``./tox.cover.sh`` script directly (or even ``./tox.cover.sh $pkg1
-3
View File
@@ -5,9 +5,6 @@
# Use a 4096 bit RSA key instead of 2048
rsa-key-size = 4096
# Always use the staging/testing server
server = https://acme-staging.api.letsencrypt.org/directory
# Uncomment and update to register with the specified e-mail address
# email = foo@example.com
+3
View File
@@ -1,3 +1,6 @@
# Always use the staging/testing server - avoids rate limiting
server = https://acme-staging.api.letsencrypt.org/directory
# This is an example configuration file for developers
config-dir = /tmp/le/conf
work-dir = /tmp/le/conf
@@ -86,18 +86,25 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
@classmethod
def add_parser_arguments(cls, add):
add("ctl", default=constants.CLI_DEFAULTS["ctl"],
help="Path to the 'apache2ctl' binary, used for 'configtest', "
"retrieving the Apache2 version number, and initialization "
"parameters.")
add("enmod", default=constants.CLI_DEFAULTS["enmod"],
add("enmod", default=constants.os_constant("enmod"),
help="Path to the Apache 'a2enmod' binary.")
add("dismod", default=constants.CLI_DEFAULTS["dismod"],
add("dismod", default=constants.os_constant("dismod"),
help="Path to the Apache 'a2dismod' binary.")
add("le-vhost-ext", default=constants.CLI_DEFAULTS["le_vhost_ext"],
add("le-vhost-ext", default=constants.os_constant("le_vhost_ext"),
help="SSL vhost configuration extension.")
add("server-root", default=constants.CLI_DEFAULTS["server_root"],
add("server-root", default=constants.os_constant("server_root"),
help="Apache server root directory.")
add("vhost-root", default=constants.os_constant("vhost_root"),
help="Apache server VirtualHost configuration root")
add("challenge-location",
default=constants.os_constant("challenge_location"),
help="Directory path for challenge configuration.")
add("handle-modules", default=constants.os_constant("handle_mods"),
help="Let installer handle enabling required modules for you." +
"(Only Ubuntu/Debian currently)")
add("handle-sites", default=constants.os_constant("handle_sites"),
help="Let installer handle enabling sites for you." +
"(Only Ubuntu/Debian currently)")
le_util.add_deprecated_argument(add, "init-script", 1)
def __init__(self, *args, **kwargs):
@@ -137,18 +144,12 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
"""
# Verify Apache is installed
for exe in (self.conf("ctl"), self.conf("enmod"), self.conf("dismod")):
if not le_util.exe_exists(exe):
raise errors.NoInstallationError
if not le_util.exe_exists(constants.os_constant("restart_cmd")[0]):
raise errors.NoInstallationError
# Make sure configuration is valid
self.config_test()
self.parser = parser.ApacheParser(
self.aug, self.conf("server-root"), self.conf("ctl"))
# 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()
@@ -156,6 +157,12 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
raise errors.NotSupportedError(
"Apache Version %s not supported.", str(self.version))
self.parser = parser.ApacheParser(
self.aug, self.conf("server-root"), self.conf("vhost-root"),
self.version)
# Check for errors in parsing files with Augeas
self.check_parsing_errors("httpd.aug")
# Get all of the available vhosts
self.vhosts = self.get_virtual_hosts()
@@ -236,9 +243,10 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
if chain_path is not None:
self.save_notes += "\tSSLCertificateChainFile %s\n" % chain_path
# Make sure vhost is enabled
if not vhost.enabled:
self.enable_site(vhost)
# Make sure vhost is enabled if distro with enabled / available
if self.conf("handle-sites"):
if not vhost.enabled:
self.enable_site(vhost)
def choose_vhost(self, target_name, temp=False):
"""Chooses a virtual host based on the given domain name.
@@ -458,7 +466,10 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
is_ssl = True
filename = get_file_path(path)
is_enabled = self.is_site_enabled(filename)
if self.conf("handle-sites"):
is_enabled = self.is_site_enabled(filename)
else:
is_enabled = True
macro = False
if "/macro/" in path.lower():
@@ -469,7 +480,6 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
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 Apache configuration.
@@ -478,10 +488,10 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
:rtype: list
"""
# Search sites-available, httpd.conf for possible virtual hosts
# Search vhost-root, httpd.conf for possible virtual hosts
paths = self.aug.match(
("/files%s/sites-available//*[label()=~regexp('%s')]" %
(self.parser.root, parser.case_i("VirtualHost"))))
("/files%s//*[label()=~regexp('%s')]" %
(self.conf("vhost-root"), parser.case_i("VirtualHost"))))
vhs = []
@@ -540,16 +550,16 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
:param str port: Port to listen on
"""
if "ssl_module" not in self.parser.modules:
self.enable_mod("ssl", temp=temp)
if self.version >= (2, 4) and "socache_shmcb_module" not in self.parser.modules:
self.enable_mod("socache_shmcb", temp=temp)
self.prepare_https_modules(temp)
# Check for Listen <port>
# Note: This could be made to also look for ip:443 combo
listens = [self.parser.get_arg(x).split()[0] for x in self.parser.find_dir("Listen")]
# In case no Listens are set (which really is a broken apache config)
if not listens:
listens = ["80"]
if port in listens:
return
for listen in listens:
# For any listen statement, check if the machine also listens on Port 443.
# If not, add such a listen statement.
@@ -584,6 +594,20 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
ip, port, self.parser.loc["listen"])
listens.append("%s:%s" % (ip, port))
def prepare_https_modules(self, temp):
"""Helper method for prepare_server_https, taking care of enabling
needed modules
:param boolean temp: If the change is temporary
"""
if self.conf("handle-modules"):
if "ssl_module" not in self.parser.modules:
self.enable_mod("ssl", temp=temp)
if self.version >= (2, 4) and ("socache_shmcb_module" not in
self.parser.modules):
self.enable_mod("socache_shmcb", temp=temp)
def make_addrs_sni_ready(self, addrs):
"""Checks to see if the server is ready for SNI challenges.
@@ -606,7 +630,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
Duplicates vhost and adds default ssl options
New vhost will reside as (nonssl_vhost.path) +
``letsencrypt_apache.constants.CLI_DEFAULTS["le_vhost_ext"]``
``letsencrypt_apache.constants.os_constant("le_vhost_ext")``
.. note:: This function saves the configuration
@@ -1068,8 +1092,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
if len(ssl_vhost.name) < (255 - (len(redirect_filename) + 1)):
redirect_filename = "le-redirect-%s.conf" % ssl_vhost.name
redirect_filepath = os.path.join(
self.parser.root, "sites-available", redirect_filename)
redirect_filepath = os.path.join(self.conf("vhost-root"), redirect_filename)
# Register the new file that will be created
# Note: always register the creation before writing to ensure file will
@@ -1155,7 +1178,14 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
:rtype: bool
"""
enabled_dir = os.path.join(self.parser.root, "sites-enabled")
if not os.path.isdir(enabled_dir):
error_msg = ("Directory '{0}' does not exist. Please ensure "
"that the values for --apache-handle-sites and "
"--apache-server-root are correct for your "
"environment.".format(enabled_dir))
raise errors.ConfigurationError(error_msg)
for entry in os.listdir(enabled_dir):
try:
if filecmp.cmp(avail_fp, os.path.join(enabled_dir, entry)):
@@ -1243,7 +1273,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
# Modules can enable additional config files. Variables may be defined
# within these new configuration sections.
# Reload is not necessary as DUMP_RUN_CFG uses latest config.
self.parser.update_runtime_variables(self.conf("ctl"))
self.parser.update_runtime_variables()
def _add_parser_mod(self, mod_name):
"""Shortcut for updating parser modules."""
@@ -1281,7 +1311,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
"""
try:
le_util.run_script([self.conf("ctl"), "-k", "graceful"])
le_util.run_script(constants.os_constant("restart_cmd"))
except errors.SubprocessError as err:
raise errors.MisconfigurationError(str(err))
@@ -1292,7 +1322,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
"""
try:
le_util.run_script([self.conf("ctl"), "configtest"])
le_util.run_script(constants.os_constant("conftest_cmd"))
except errors.SubprocessError as err:
raise errors.MisconfigurationError(str(err))
@@ -1308,10 +1338,12 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
"""
try:
stdout, _ = le_util.run_script([self.conf("ctl"), "-v"])
stdout, _ = le_util.run_script(
constants.os_constant("version_cmd"))
except errors.SubprocessError:
raise errors.PluginError(
"Unable to run %s -v" % self.conf("ctl"))
"Unable to run %s -v" %
constants.os_constant("version_cmd"))
regex = re.compile(r"Apache/([0-9\.]*)", re.IGNORECASE)
matches = regex.findall(stdout)
@@ -1,14 +1,62 @@
"""Apache plugin constants."""
import pkg_resources
from letsencrypt import le_util
CLI_DEFAULTS = dict(
CLI_DEFAULTS_DEBIAN = dict(
server_root="/etc/apache2",
ctl="apache2ctl",
vhost_root="/etc/apache2/sites-available",
vhost_files="*",
version_cmd=['apache2ctl', '-v'],
define_cmd=['apache2ctl', '-t', '-D', 'DUMP_RUN_CFG'],
restart_cmd=['apache2ctl', 'graceful'],
conftest_cmd=['apache2ctl', 'configtest'],
enmod="a2enmod",
dismod="a2dismod",
le_vhost_ext="-le-ssl.conf",
handle_mods=True,
handle_sites=True,
challenge_location="/etc/apache2"
)
CLI_DEFAULTS_CENTOS = dict(
server_root="/etc/httpd",
vhost_root="/etc/httpd/conf.d",
vhost_files="*.conf",
version_cmd=['apachectl', '-v'],
define_cmd=['apachectl', '-t', '-D', 'DUMP_RUN_CFG'],
restart_cmd=['apachectl', 'graceful'],
conftest_cmd=['apachectl', 'configtest'],
enmod=None,
dismod=None,
le_vhost_ext="-le-ssl.conf",
handle_mods=False,
handle_sites=False,
challenge_location="/etc/httpd/conf.d"
)
CLI_DEFAULTS_GENTOO = dict(
server_root="/etc/apache2",
vhost_root="/etc/apache2/vhosts.d",
vhost_files="*.conf",
version_cmd=['/usr/sbin/apache2', '-v'],
define_cmd=['/usr/sbin/apache2', '-t', '-D', 'DUMP_RUN_CFG'],
restart_cmd=['apache2ctl', 'graceful'],
conftest_cmd=['apache2ctl', 'configtest'],
enmod=None,
dismod=None,
le_vhost_ext="-le-ssl.conf",
handle_mods=False,
handle_sites=False,
challenge_location="/etc/apache2/vhosts.d"
)
CLI_DEFAULTS = {
"debian": CLI_DEFAULTS_DEBIAN,
"ubuntu": CLI_DEFAULTS_DEBIAN,
"centos": CLI_DEFAULTS_CENTOS,
"centos linux": CLI_DEFAULTS_CENTOS,
"fedora": CLI_DEFAULTS_CENTOS,
"red hat enterprise linux server": CLI_DEFAULTS_CENTOS,
"gentoo base system": CLI_DEFAULTS_GENTOO
}
"""CLI defaults."""
MOD_SSL_CONF_DEST = "options-ssl-apache.conf"
@@ -33,7 +81,7 @@ REWRITE_HTTPS_ARGS_WITH_END = [
https vhost"""
HSTS_ARGS = ["always", "set", "Strict-Transport-Security",
"\"max-age=31536000; includeSubDomains\""]
"\"max-age=31536000\""]
"""Apache header arguments for HSTS"""
UIR_ARGS = ["always", "set", "Content-Security-Policy",
@@ -42,3 +90,15 @@ UIR_ARGS = ["always", "set", "Content-Security-Policy",
HEADER_ARGS = {"Strict-Transport-Security": HSTS_ARGS,
"Upgrade-Insecure-Requests": UIR_ARGS}
def os_constant(key):
"""Get a constant value for operating system
:param key: name of cli constant
:return: value of constant for active os
"""
os_info = le_util.get_os_info()
try:
constants = CLI_DEFAULTS[os_info[0].lower()]
except KeyError:
constants = CLI_DEFAULTS["debian"]
return constants[key]
@@ -14,9 +14,9 @@ SSLOptions +StrictRequire
LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-agent}i\"" vhost_combined
LogFormat "%v %h %l %u %t \"%r\" %>s %b" vhost_common
CustomLog /var/log/apache2/access.log vhost_combined
LogLevel warn
ErrorLog /var/log/apache2/error.log
#CustomLog /var/log/apache2/access.log vhost_combined
#LogLevel warn
#ErrorLog /var/log/apache2/error.log
# Always ensure Cookies have "Secure" set (JAH 2012/1)
#Header edit Set-Cookie (?i)^(.*)(;\s*secure)??((\s*;)?(.*)) "$1; Secure$3$4"
+21 -18
View File
@@ -8,6 +8,7 @@ import subprocess
from letsencrypt import errors
from letsencrypt_apache import constants
logger = logging.getLogger(__name__)
@@ -27,7 +28,7 @@ class ApacheParser(object):
arg_var_interpreter = re.compile(r"\$\{[^ \}]*}")
fnmatch_chars = set(["*", "?", "\\", "[", "]"])
def __init__(self, aug, root, ctl):
def __init__(self, aug, root, vhostroot, version=(2, 4)):
# Note: Order is important here.
# This uses the binary, so it can be done first.
@@ -35,8 +36,8 @@ class ApacheParser(object):
# https://httpd.apache.org/docs/2.4/mod/core.html#ifdefine
# This only handles invocation parameters and Define directives!
self.variables = {}
self.unparsable = False
self.update_runtime_variables(ctl)
if version >= (2, 4):
self.update_runtime_variables()
self.aug = aug
# Find configuration root and make sure augeas can parse it.
@@ -44,6 +45,8 @@ class ApacheParser(object):
self.loc = {"root": self._find_config_root()}
self._parse_file(self.loc["root"])
self.vhostroot = os.path.abspath(vhostroot)
# This problem has been fixed in Augeas 1.0
self.standardize_excl()
@@ -56,12 +59,12 @@ class ApacheParser(object):
# Set up rest of locations
self.loc.update(self._set_locations())
# 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") + "/*")
# Must also attempt to parse virtual host root
self._parse_file(self.vhostroot + "/" +
constants.os_constant("vhost_files"))
#check to see if there were unparsed define statements
if self.unparsable:
# check to see if there were unparsed define statements
if version < (2, 4):
if self.find_dir("Define", exclude=False):
raise errors.PluginError("Error parsing runtime variables")
@@ -89,7 +92,7 @@ class ApacheParser(object):
self.modules.add(
os.path.basename(self.get_arg(match_filename))[:-2] + "c")
def update_runtime_variables(self, ctl):
def update_runtime_variables(self):
""""
.. note:: Compile time variables (apache2ctl -V) are not used within the
@@ -99,20 +102,19 @@ class ApacheParser(object):
.. todo:: Create separate compile time variables... simply for arg_get()
"""
stdout = self._get_runtime_cfg(ctl)
stdout = self._get_runtime_cfg()
variables = dict()
matches = re.compile(r"Define: ([^ \n]*)").findall(stdout)
try:
matches.remove("DUMP_RUN_CFG")
except ValueError:
self.unparsable = True
return
for match in matches:
if match.count("=") > 1:
logger.error("Unexpected number of equal signs in "
"apache2ctl -D DUMP_RUN_CFG")
"runtime config dump.")
raise errors.PluginError(
"Error parsing Apache runtime variables")
parts = match.partition("=")
@@ -120,7 +122,7 @@ class ApacheParser(object):
self.variables = variables
def _get_runtime_cfg(self, ctl): # pylint: disable=no-self-use
def _get_runtime_cfg(self): # pylint: disable=no-self-use
"""Get runtime configuration info.
:returns: stdout from DUMP_RUN_CFG
@@ -128,16 +130,18 @@ class ApacheParser(object):
"""
try:
proc = subprocess.Popen(
[ctl, "-t", "-D", "DUMP_RUN_CFG"],
constants.os_constant("define_cmd"),
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
stdout, stderr = proc.communicate()
except (OSError, ValueError):
logger.error(
"Error accessing %s for runtime parameters!%s", ctl, os.linesep)
"Error running command %s for runtime parameters!%s",
constants.os_constant("define_cmd"), os.linesep)
raise errors.MisconfigurationError(
"Error accessing loaded Apache parameters: %s", ctl)
"Error accessing loaded Apache parameters: %s",
constants.os_constant("define_cmd"))
# Small errors that do not impede
if proc.returncode != 0:
logger.warn("Error in checking parameter list: %s", stderr)
@@ -552,8 +556,7 @@ class ApacheParser(object):
def _find_config_root(self):
"""Find the Apache Configuration Root file."""
location = ["apache2.conf", "httpd.conf"]
location = ["apache2.conf", "httpd.conf", "conf/httpd.conf"]
for name in location:
if os.path.isfile(os.path.join(self.root, name)):
return os.path.join(self.root, name)
@@ -0,0 +1,78 @@
#!/bin/bash
# A hackish script to see if the client is behaving as expected
# with each of the "passing" conf files.
export EA=/etc/apache2/
TESTDIR="`dirname $0`"
LEROOT="`realpath \"$TESTDIR/../../../../\"`"
cd $TESTDIR/passing
LETSENCRYPT="${LETSENCRYPT:-$LEROOT/venv/bin/letsencrypt}"
function CleanupExit() {
echo control c, exiting tests...
if [ "$f" != "" ] ; then
Cleanup
fi
exit 1
}
function Setup() {
if [ "$APPEND_APACHECONF" = "" ] ; then
sudo cp "$f" "$EA"/sites-available/
sudo ln -sf "$EA/sites-available/$f" "$EA/sites-enabled/$f"
sudo echo """
<VirtualHost *:80>
ServerName example.com
DocumentRoot /tmp/
ErrorLog /tmp/error.log
CustomLog /tmp/requests.log combined
</VirtualHost>""" >> $EA/sites-available/throwaway-example.conf
else
TMP="/tmp/`basename \"$APPEND_APACHECONF\"`.$$"
sudo cp -a "$APPEND_APACHECONF" "$TMP"
sudo bash -c "cat \"$f\" >> \"$APPEND_APACHECONF\""
fi
}
function Cleanup() {
if [ "$APPEND_APACHECONF" = "" ] ; then
sudo rm /etc/apache2/sites-{enabled,available}/"$f"
sudo rm $EA/sites-available/throwaway-example.conf
else
sudo mv "$TMP" "$APPEND_APACHECONF"
fi
}
# if our environment asks us to enable modules, do our best!
if [ "$1" = --debian-modules ] ; then
sudo apt-get install -y libapache2-mod-wsgi
sudo apt-get install -y libapache2-mod-macro
for mod in ssl rewrite macro wsgi deflate userdir version mime ; do
sudo a2enmod $mod
done
fi
FAILS=0
trap CleanupExit INT
for f in *.conf ; do
echo -n testing "$f"...
Setup
RESULT=`echo c | sudo "$LETSENCRYPT" -vvvv --debug --staging --apache --register-unsafely-without-email --agree-tos certonly -t 2>&1`
if echo $RESULT | grep -Eq \("Which names would you like"\|"mod_macro is not yet"\) ; then
echo passed
else
echo failed
echo $RESULT
echo
echo
FAILS=`expr $FAILS + 1`
fi
Cleanup
done
if [ "$FAILS" -ne 0 ] ; then
exit 1
fi
exit 0
@@ -0,0 +1,6 @@
# Modules required to parse these conf files:
ssl
rewrite
macro
wsgi
deflate
@@ -1,6 +1,6 @@
<VirtualHost *:80>
WSGIDaemonProcess _graphite processes=5 threads=5 display-name='%{GROUP}' inactivity-timeout=120 user=_graphite group=_graphite
WSGIDaemonProcess _graphite processes=5 threads=5 display-name='%{GROUP}' inactivity-timeout=120 user=www-data group=www-data
WSGIProcessGroup _graphite
WSGIImportScript /usr/share/graphite-web/graphite.wsgi process-group=_graphite application-group=%{GLOBAL}
WSGIScriptAlias / /usr/share/graphite-web/graphite.wsgi
@@ -17,7 +17,7 @@ class AugeasConfiguratorTest(util.ApacheTest):
super(AugeasConfiguratorTest, self).setUp()
self.config = util.get_apache_configurator(
self.config_path, self.config_dir, self.work_dir)
self.config_path, self.vhost_path, self.config_dir, self.work_dir)
self.vh_truth = util.get_vh_truth(
self.temp_dir, "debian_apache_2_4/two_vhost_80")
@@ -27,7 +27,7 @@ class TwoVhost80Test(util.ApacheTest):
super(TwoVhost80Test, self).setUp()
self.config = util.get_apache_configurator(
self.config_path, self.config_dir, self.work_dir)
self.config_path, self.vhost_path, self.config_dir, self.work_dir)
self.config = self.mock_deploy_cert(self.config)
self.vh_truth = util.get_vh_truth(
self.temp_dir, "debian_apache_2_4/two_vhost_80")
@@ -127,6 +127,24 @@ class TwoVhost80Test(util.ApacheTest):
self.assertEqual(found, 6)
# Handle case of non-debian layout get_virtual_hosts
orig_conf = self.config.conf
with mock.patch(
"letsencrypt_apache.configurator.ApacheConfigurator.conf"
) as mock_conf:
def conf_sideeffect(key):
"""Handle calls to configurator.conf()
:param key: configuration key
:return: configuration value
"""
if key == "handle-sites":
return False
else:
return orig_conf(key)
mock_conf.side_effect = conf_sideeffect
vhs = self.config.get_virtual_hosts()
self.assertEqual(len(vhs), 6)
@mock.patch("letsencrypt_apache.display_ops.select_vhost")
def test_choose_vhost_none_avail(self, mock_select):
mock_select.return_value = None
@@ -212,6 +230,11 @@ class TwoVhost80Test(util.ApacheTest):
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))
with mock.patch("os.path.isdir") as mock_isdir:
mock_isdir.return_value = False
self.assertRaises(errors.ConfigurationError,
self.config.is_site_enabled,
"irrelevant")
@mock.patch("letsencrypt.le_util.run_script")
@mock.patch("letsencrypt.le_util.exe_exists")
@@ -255,7 +278,7 @@ class TwoVhost80Test(util.ApacheTest):
def test_deploy_cert_newssl(self):
self.config = util.get_apache_configurator(
self.config_path, self.config_dir, self.work_dir, version=(2, 4, 16))
self.config_path, self.vhost_path, self.config_dir, self.work_dir, version=(2, 4, 16))
self.config.parser.modules.add("ssl_module")
self.config.parser.modules.add("mod_ssl.c")
@@ -288,7 +311,7 @@ class TwoVhost80Test(util.ApacheTest):
def test_deploy_cert_newssl_no_fullchain(self):
self.config = util.get_apache_configurator(
self.config_path, self.config_dir, self.work_dir, version=(2, 4, 16))
self.config_path, self.vhost_path, self.config_dir, self.work_dir, version=(2, 4, 16))
self.config = self.mock_deploy_cert(self.config)
self.config.parser.modules.add("ssl_module")
@@ -302,7 +325,7 @@ class TwoVhost80Test(util.ApacheTest):
def test_deploy_cert_old_apache_no_chain(self):
self.config = util.get_apache_configurator(
self.config_path, self.config_dir, self.work_dir, version=(2, 4, 7))
self.config_path, self.vhost_path, self.config_dir, self.work_dir, version=(2, 4, 7))
self.config = self.mock_deploy_cert(self.config)
self.config.parser.modules.add("ssl_module")
@@ -438,6 +461,25 @@ class TwoVhost80Test(util.ApacheTest):
self.assertEqual(mock_add_dir.call_args_list[1][0][2], ["[::1]:8080", "https"])
self.assertEqual(mock_add_dir.call_args_list[2][0][2], ["1.1.1.1:8080", "https"])
def test_prepare_server_https_mixed_listen(self):
mock_find = mock.Mock()
mock_find.return_value = ["test1", "test2"]
mock_get = mock.Mock()
mock_get.side_effect = ["1.2.3.4:8080", "443"]
mock_add_dir = mock.Mock()
mock_enable = mock.Mock()
self.config.parser.find_dir = mock_find
self.config.parser.get_arg = mock_get
self.config.parser.add_dir_to_ifmodssl = mock_add_dir
self.config.enable_mod = mock_enable
# Test Listen statements with specific ip listeed
self.config.prepare_server_https("443")
# Should only be 2 here, as the third interface already listens to the correct port
self.assertEqual(mock_add_dir.call_count, 0)
def test_make_vhost_ssl(self):
ssl_vhost = self.config.make_vhost_ssl(self.vh_truth[0])
@@ -0,0 +1,27 @@
"""Test for letsencrypt_apache.configurator."""
import mock
import unittest
from letsencrypt_apache import constants
class ConstantsTest(unittest.TestCase):
@mock.patch("letsencrypt.le_util.get_os_info")
def test_get_debian_value(self, os_info):
os_info.return_value = ('Debian', '', '')
self.assertEqual(constants.os_constant("vhost_root"),
"/etc/apache2/sites-available")
@mock.patch("letsencrypt.le_util.get_os_info")
def test_get_centos_value(self, os_info):
os_info.return_value = ('CentOS Linux', '', '')
self.assertEqual(constants.os_constant("vhost_root"),
"/etc/httpd/conf.d")
@mock.patch("letsencrypt.le_util.get_os_info")
def test_get_default_value(self, os_info):
os_info.return_value = ('Nonexistent Linux', '', '')
self.assertEqual(constants.os_constant("vhost_root"),
"/etc/apache2/sites-available")
@@ -145,25 +145,26 @@ class BasicParserTest(util.ParserTest):
expected_vars = {"TEST": "", "U_MICH": "", "TLS": "443",
"example_path": "Documents/path"}
self.parser.update_runtime_variables("ctl")
self.parser.update_runtime_variables()
self.assertEqual(self.parser.variables, expected_vars)
@mock.patch("letsencrypt_apache.parser.ApacheParser._get_runtime_cfg")
def test_update_runtime_vars_bad_output(self, mock_cfg):
mock_cfg.return_value = "Define: TLS=443=24"
self.parser.update_runtime_variables("ctl")
self.assertTrue(self.parser.unparsable)
self.parser.update_runtime_variables()
mock_cfg.return_value = "Define: DUMP_RUN_CFG\nDefine: TLS=443=24"
self.assertRaises(
errors.PluginError, self.parser.update_runtime_variables, "ctl")
errors.PluginError, self.parser.update_runtime_variables)
@mock.patch("letsencrypt_apache.constants.os_constant")
@mock.patch("letsencrypt_apache.parser.subprocess.Popen")
def test_update_runtime_vars_bad_ctl(self, mock_popen):
def test_update_runtime_vars_bad_ctl(self, mock_popen, mock_const):
mock_popen.side_effect = OSError
mock_const.return_value = "nonexistent"
self.assertRaises(
errors.MisconfigurationError,
self.parser.update_runtime_variables, "ctl")
self.parser.update_runtime_variables)
@mock.patch("letsencrypt_apache.parser.subprocess.Popen")
def test_update_runtime_vars_bad_exit(self, mock_popen):
@@ -171,7 +172,7 @@ class BasicParserTest(util.ParserTest):
mock_popen.returncode = -1
self.assertRaises(
errors.MisconfigurationError,
self.parser.update_runtime_variables, "ctl")
self.parser.update_runtime_variables)
class ParserInitTest(util.ApacheTest):
@@ -188,17 +189,11 @@ class ParserInitTest(util.ApacheTest):
@mock.patch("letsencrypt_apache.parser.ApacheParser._get_runtime_cfg")
def test_unparsable(self, mock_cfg):
from letsencrypt_apache.parser import ApacheParser
def unparsable_true(self, arg):
"""a helper to set the self unparsabale to true"""
print "side effect has passed in arg: %s", arg
self.unparsable = True
with mock.patch.object(ApacheParser, 'update_runtime_variables', autospec=True) as urv:
urv.side_effect = unparsable_true
mock_cfg.return_value = ('Define: TEST')
self.assertRaises(
errors.PluginError,
ApacheParser, self.aug, os.path.relpath(self.config_path), "ctl")
self.assertEquals(1, 1)
mock_cfg.return_value = ('Define: TEST')
self.assertRaises(
errors.PluginError,
ApacheParser, self.aug, os.path.relpath(self.config_path),
"/dummy/vhostpath", version=(2, 2, 22))
def test_root_normalized(self):
from letsencrypt_apache.parser import ApacheParser
@@ -208,7 +203,9 @@ class ParserInitTest(util.ApacheTest):
path = os.path.join(
self.temp_dir,
"debian_apache_2_4/////two_vhost_80/../two_vhost_80/apache2")
parser = ApacheParser(self.aug, path, "dummy_ctl")
parser = ApacheParser(self.aug, path,
"/dummy/vhostpath")
self.assertEqual(parser.root, self.config_path)
@@ -217,7 +214,8 @@ class ParserInitTest(util.ApacheTest):
with mock.patch("letsencrypt_apache.parser.ApacheParser."
"update_runtime_variables"):
parser = ApacheParser(
self.aug, os.path.relpath(self.config_path), "dummy_ctl")
self.aug, os.path.relpath(self.config_path),
"/dummy/vhostpath")
self.assertEqual(parser.root, self.config_path)
@@ -226,7 +224,8 @@ class ParserInitTest(util.ApacheTest):
with mock.patch("letsencrypt_apache.parser.ApacheParser."
"update_runtime_variables"):
parser = ApacheParser(
self.aug, self.config_path + os.path.sep, "dummy_ctl")
self.aug, self.config_path + os.path.sep,
"/dummy/vhostpath")
self.assertEqual(parser.root, self.config_path)
@@ -20,7 +20,7 @@ class TlsSniPerformTest(util.ApacheTest):
super(TlsSniPerformTest, self).setUp()
config = util.get_apache_configurator(
self.config_path, self.config_dir, self.work_dir)
self.config_path, self.vhost_path, self.config_dir, self.work_dir)
config.config.tls_sni_01_port = 443
from letsencrypt_apache import tls_sni_01
@@ -23,7 +23,8 @@ from letsencrypt_apache import obj
class ApacheTest(unittest.TestCase): # pylint: disable=too-few-public-methods
def setUp(self, test_dir="debian_apache_2_4/two_vhost_80",
config_root="debian_apache_2_4/two_vhost_80/apache2"):
config_root="debian_apache_2_4/two_vhost_80/apache2",
vhost_root="debian_apache_2_4/two_vhost_80/apache2/sites-available"):
# pylint: disable=arguments-differ
super(ApacheTest, self).setUp()
@@ -36,6 +37,7 @@ class ApacheTest(unittest.TestCase): # pylint: disable=too-few-public-methods
constants.MOD_SSL_CONF_DEST)
self.config_path = os.path.join(self.temp_dir, config_root)
self.vhost_path = os.path.join(self.temp_dir, vhost_root)
self.rsa512jwk = jose.JWKRSA.load(test_util.load_vector(
"rsa512_key.pem"))
@@ -44,8 +46,9 @@ class ApacheTest(unittest.TestCase): # pylint: disable=too-few-public-methods
class ParserTest(ApacheTest): # pytlint: disable=too-few-public-methods
def setUp(self, test_dir="debian_apache_2_4/two_vhost_80",
config_root="debian_apache_2_4/two_vhost_80/apache2"):
super(ParserTest, self).setUp(test_dir, config_root)
config_root="debian_apache_2_4/two_vhost_80/apache2",
vhost_root="debian_apache_2_4/two_vhost_80/apache2/sites-available"):
super(ParserTest, self).setUp(test_dir, config_root, vhost_root)
zope.component.provideUtility(display_util.FileDisplay(sys.stdout))
@@ -55,11 +58,11 @@ class ParserTest(ApacheTest): # pytlint: disable=too-few-public-methods
with mock.patch("letsencrypt_apache.parser.ApacheParser."
"update_runtime_variables"):
self.parser = ApacheParser(
self.aug, self.config_path, "dummy_ctl_path")
self.aug, self.config_path, self.vhost_path)
def get_apache_configurator(
config_path, config_dir, work_dir, version=(2, 4, 7), conf=None):
config_path, vhost_path, config_dir, work_dir, version=(2, 4, 7), conf=None):
"""Create an Apache Configurator with the specified options.
:param conf: Function that returns binary paths. self.conf in Configurator
@@ -68,7 +71,9 @@ def get_apache_configurator(
backups = os.path.join(work_dir, "backups")
mock_le_config = mock.MagicMock(
apache_server_root=config_path,
apache_le_vhost_ext=constants.CLI_DEFAULTS["le_vhost_ext"],
apache_vhost_root=vhost_path,
apache_le_vhost_ext=constants.os_constant("le_vhost_ext"),
apache_challenge_location=config_path,
backup_dir=backups,
config_dir=config_dir,
temp_checkpoint_dir=os.path.join(work_dir, "temp_checkpoints"),
@@ -50,7 +50,7 @@ class ApacheTlsSni01(common.TLSSNI01):
super(ApacheTlsSni01, self).__init__(*args, **kwargs)
self.challenge_conf = os.path.join(
self.configurator.conf("server-root"),
self.configurator.conf("challenge-location"),
"le_tls_sni_01_cert_challenge.conf")
def perform(self):
@@ -15,11 +15,12 @@ from acme import crypto_util
from acme import messages
from letsencrypt import achallenges
from letsencrypt import errors as le_errors
from letsencrypt import validator
from letsencrypt.tests import acme_util
from letsencrypt_compatibility_test import errors
from letsencrypt_compatibility_test import util
from letsencrypt_compatibility_test import validator
from letsencrypt_compatibility_test.configurators.apache import apache24
@@ -1,4 +1,4 @@
"""Tests for letsencrypt.validator."""
"""Tests for letsencrypt_compatibility_test.validator."""
import requests
import unittest
@@ -6,28 +6,31 @@ import mock
import OpenSSL
from acme import errors as acme_errors
from letsencrypt import validator
from letsencrypt_compatibility_test import validator
class ValidatorTest(unittest.TestCase):
def setUp(self):
self.validator = validator.Validator()
@mock.patch("letsencrypt.validator.crypto_util.probe_sni")
@mock.patch(
"letsencrypt_compatibility_test.validator.crypto_util.probe_sni")
def test_certificate_success(self, mock_probe_sni):
cert = OpenSSL.crypto.X509()
mock_probe_sni.return_value = cert
self.assertTrue(self.validator.certificate(
cert, "test.com", "127.0.0.1"))
@mock.patch("letsencrypt.validator.crypto_util.probe_sni")
@mock.patch(
"letsencrypt_compatibility_test.validator.crypto_util.probe_sni")
def test_certificate_error(self, mock_probe_sni):
cert = OpenSSL.crypto.X509()
mock_probe_sni.side_effect = [acme_errors.Error]
self.assertFalse(self.validator.certificate(
cert, "test.com", "127.0.0.1"))
@mock.patch("letsencrypt.validator.crypto_util.probe_sni")
@mock.patch(
"letsencrypt_compatibility_test.validator.crypto_util.probe_sni")
def test_certificate_failure(self, mock_probe_sni):
cert = OpenSSL.crypto.X509()
cert.set_serial_number(1337)
@@ -35,67 +38,67 @@ class ValidatorTest(unittest.TestCase):
self.assertFalse(self.validator.certificate(
cert, "test.com", "127.0.0.1"))
@mock.patch("letsencrypt.validator.requests.get")
@mock.patch("letsencrypt_compatibility_test.validator.requests.get")
def test_succesful_redirect(self, mock_get_request):
mock_get_request.return_value = create_response(
301, {"location": "https://test.com"})
self.assertTrue(self.validator.redirect("test.com"))
@mock.patch("letsencrypt.validator.requests.get")
@mock.patch("letsencrypt_compatibility_test.validator.requests.get")
def test_redirect_with_headers(self, mock_get_request):
mock_get_request.return_value = create_response(
301, {"location": "https://test.com"})
self.assertTrue(self.validator.redirect(
"test.com", headers={"Host": "test.com"}))
@mock.patch("letsencrypt.validator.requests.get")
@mock.patch("letsencrypt_compatibility_test.validator.requests.get")
def test_redirect_missing_location(self, mock_get_request):
mock_get_request.return_value = create_response(301)
self.assertFalse(self.validator.redirect("test.com"))
@mock.patch("letsencrypt.validator.requests.get")
@mock.patch("letsencrypt_compatibility_test.validator.requests.get")
def test_redirect_wrong_status_code(self, mock_get_request):
mock_get_request.return_value = create_response(
201, {"location": "https://test.com"})
self.assertFalse(self.validator.redirect("test.com"))
@mock.patch("letsencrypt.validator.requests.get")
@mock.patch("letsencrypt_compatibility_test.validator.requests.get")
def test_redirect_wrong_redirect_code(self, mock_get_request):
mock_get_request.return_value = create_response(
303, {"location": "https://test.com"})
self.assertFalse(self.validator.redirect("test.com"))
@mock.patch("letsencrypt.validator.requests.get")
@mock.patch("letsencrypt_compatibility_test.validator.requests.get")
def test_hsts_empty(self, mock_get_request):
mock_get_request.return_value = create_response(
headers={"strict-transport-security": ""})
self.assertFalse(self.validator.hsts("test.com"))
@mock.patch("letsencrypt.validator.requests.get")
@mock.patch("letsencrypt_compatibility_test.validator.requests.get")
def test_hsts_malformed(self, mock_get_request):
mock_get_request.return_value = create_response(
headers={"strict-transport-security": "sdfal"})
self.assertFalse(self.validator.hsts("test.com"))
@mock.patch("letsencrypt.validator.requests.get")
@mock.patch("letsencrypt_compatibility_test.validator.requests.get")
def test_hsts_bad_max_age(self, mock_get_request):
mock_get_request.return_value = create_response(
headers={"strict-transport-security": "max-age=not-an-int"})
self.assertFalse(self.validator.hsts("test.com"))
@mock.patch("letsencrypt.validator.requests.get")
@mock.patch("letsencrypt_compatibility_test.validator.requests.get")
def test_hsts_expire(self, mock_get_request):
mock_get_request.return_value = create_response(
headers={"strict-transport-security": "max-age=3600"})
self.assertFalse(self.validator.hsts("test.com"))
@mock.patch("letsencrypt.validator.requests.get")
@mock.patch("letsencrypt_compatibility_test.validator.requests.get")
def test_hsts(self, mock_get_request):
mock_get_request.return_value = create_response(
headers={"strict-transport-security": "max-age=31536000"})
self.assertTrue(self.validator.hsts("test.com"))
@mock.patch("letsencrypt.validator.requests.get")
@mock.patch("letsencrypt_compatibility_test.validator.requests.get")
def test_hsts_include_subdomains(self, mock_get_request):
mock_get_request.return_value = create_response(
headers={"strict-transport-security":
+6
View File
@@ -10,6 +10,7 @@ install_requires = [
'letsencrypt=={0}'.format(version),
'letsencrypt-apache=={0}'.format(version),
'docker-py',
'requests',
'zope.interface',
]
@@ -18,6 +19,11 @@ if sys.version_info < (2, 7):
else:
install_requires.append('mock')
if sys.version_info < (2, 7, 9):
# For secure SSL connexion with Python 2.7 (InsecurePlatformWarning)
install_requires.append('ndg-httpsclient')
install_requires.append('pyasn1')
docs_extras = [
'repoze.sphinx.autointerface',
'Sphinx>=1.0', # autodoc_member_order = 'bysource', autodoc_default_flags
@@ -311,17 +311,11 @@ class NginxConfigurator(common.Plugin):
"""
snakeoil_cert, snakeoil_key = self._get_snakeoil_paths()
ssl_block = [['listen', '{0} ssl'.format(self.config.tls_sni_01_port)],
# access and error logs necessary for integration
# testing (non-root)
['access_log', os.path.join(
self.config.work_dir, 'access.log')],
['error_log', os.path.join(
self.config.work_dir, 'error.log')],
['ssl_certificate', snakeoil_cert],
['ssl_certificate_key', snakeoil_key],
['include', self.parser.loc["ssl_options"]]]
self.parser.add_server_directives(
vhost.filep, vhost.names, ssl_block)
vhost.filep, vhost.names, ssl_block, replace=False)
vhost.ssl = True
vhost.raw.extend(ssl_block)
vhost.addrs.add(obj.Addr(
@@ -384,7 +378,7 @@ class NginxConfigurator(common.Plugin):
[['return', '301 https://$host$request_uri']]
]]
self.parser.add_server_directives(
vhost.filep, vhost.names, redirect_block)
vhost.filep, vhost.names, redirect_block, replace=False)
logger.info("Redirecting all traffic to ssl in %s", vhost.filep)
######################################
@@ -393,11 +387,10 @@ class NginxConfigurator(common.Plugin):
def restart(self):
"""Restarts nginx server.
:returns: Success
:rtype: bool
:raises .errors.MisconfigurationError: If either the reload fails.
"""
return nginx_restart(self.conf('ctl'), self.nginx_conf)
nginx_restart(self.conf('ctl'), self.nginx_conf)
def config_test(self): # pylint: disable=no-self-use
"""Check the configuration of Nginx for errors.
@@ -631,19 +624,16 @@ def nginx_restart(nginx_ctl, nginx_conf="/etc/nginx.conf"):
if nginx_proc.returncode != 0:
# Enter recovery routine...
logger.error("Nginx Restart Failed!\n%s\n%s", stdout, stderr)
return False
raise errors.MisconfigurationError(
"nginx restart failed:\n%s\n%s" % (stdout, stderr))
except (OSError, ValueError):
logger.fatal("Nginx Restart Failed - Please Check the Configuration")
sys.exit(1)
raise errors.MisconfigurationError("nginx restart failed")
# Nginx can take a moment to recognize a newly added TLS SNI servername, so sleep
# for a second. TODO: Check for expected servername and loop until it
# appears or return an error if looping too long.
time.sleep(1)
return True
def temp_install(options_ssl):
"""Temporary install for convenience."""
+60 -25
View File
@@ -213,6 +213,7 @@ class NginxParser(object):
if ext:
filename = filename + os.path.extsep + ext
try:
logger.debug('Dumping to %s:\n%s', filename, nginxparser.dumps(tree))
with open(filename, 'w') as _file:
nginxparser.dump(tree, _file)
except IOError:
@@ -252,7 +253,7 @@ class NginxParser(object):
return server_names == names
def add_server_directives(self, filename, names, directives,
replace=False):
replace):
"""Add or replace directives in the first server block with names.
..note :: If replace is True, this raises a misconfiguration error
@@ -269,20 +270,27 @@ class NginxParser(object):
:param bool replace: Whether to only replace existing directives
"""
_do_for_subarray(self.parsed[filename],
lambda x: self._has_server_names(x, names),
lambda x: _add_directives(x, directives, replace))
try:
_do_for_subarray(self.parsed[filename],
lambda x: self._has_server_names(x, names),
lambda x: _add_directives(x, directives, replace))
except errors.MisconfigurationError as err:
raise errors.MisconfigurationError("Problem in %s: %s" % (filename, err.message))
def add_http_directives(self, filename, directives):
"""Adds directives to the first encountered HTTP block in filename.
We insert new directives at the top of the block to work around
https://trac.nginx.org/nginx/ticket/810: If the first server block
doesn't enable OCSP stapling, stapling is broken for all blocks.
:param str filename: The absolute filename of the config file
:param list directives: The directives to add
"""
_do_for_subarray(self.parsed[filename],
lambda x: x[0] == ['http'],
lambda x: _add_directives(x[1], [directives], False))
lambda x: x[1].insert(0, directives))
def get_all_certs_keys(self):
"""Gets all certs and keys in the nginx config.
@@ -467,9 +475,14 @@ def _parse_server(server):
return parsed_server
def _add_directives(block, directives, replace=False):
"""Adds or replaces directives in a block. If the directive doesn't exist in
the entry already, raises a misconfiguration error.
def _add_directives(block, directives, replace):
"""Adds or replaces directives in a config block.
When replace=False, it's an error to try and add a directive that already
exists in the config block with a conflicting value.
When replace=True, a directive with the same name MUST already exist in the
config block, and the first instance will be replaced.
..todo :: Find directives that are in included files.
@@ -478,21 +491,43 @@ def _add_directives(block, directives, replace=False):
"""
for directive in directives:
if not replace:
# We insert new directives at the top of the block, mostly
# to work around https://trac.nginx.org/nginx/ticket/810
# Only add directive if its not already in the block
if directive not in block:
block.insert(0, directive)
else:
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:
_add_directive(block, directive, replace)
repeatable_directives = set(['server_name', 'listen', 'include'])
def _add_directive(block, directive, replace):
"""Adds or replaces a single directive in a config block.
See _add_directives for more documentation.
"""
location = -1
# Find the index of a config line where the name of the directive matches
# the name of the directive we want to add.
for index, line in enumerate(block):
if len(line) > 0 and line[0] == directive[0]:
location = index
break
if replace:
if location == -1:
raise errors.MisconfigurationError(
'expected directive for %s in the Nginx '
'config but did not find it.' % directive[0])
block[location] = directive
else:
# Append directive. Fail if the name is not a repeatable directive name,
# and there is already a copy of that directive with a different value
# in the config file.
directive_name = directive[0]
directive_value = directive[1]
if location != -1 and directive_name.__str__() not in repeatable_directives:
if block[location][1] == directive_value:
# There's a conflict, but the existing value matches the one we
# want to insert, so it's fine.
pass
else:
raise errors.MisconfigurationError(
'Let\'s Encrypt expected directive for %s in the Nginx '
'config but did not find it.' % directive[0])
'tried to insert directive "%s" but found conflicting "%s".' % (
directive, block[location]))
else:
block.append(directive)
@@ -40,6 +40,23 @@ class NginxConfiguratorTest(util.NginxTest):
self.assertEquals((1, 6, 2), self.config.version)
self.assertEquals(5, len(self.config.parser.parsed))
@mock.patch("letsencrypt_nginx.configurator.le_util.exe_exists")
@mock.patch("letsencrypt_nginx.configurator.subprocess.Popen")
def test_prepare_initializes_version(self, mock_popen, mock_exe_exists):
mock_popen().communicate.return_value = (
"", "\n".join(["nginx version: nginx/1.6.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"]))
mock_exe_exists.return_value = True
self.config.version = None
self.config.prepare()
self.assertEquals((1, 6, 2), self.config.version)
@mock.patch("letsencrypt_nginx.configurator.socket.gethostbyaddr")
def test_get_all_names(self, mock_gethostbyaddr):
mock_gethostbyaddr.return_value = ('155.225.50.69.nephoscale.net', [], [])
@@ -65,16 +82,19 @@ class NginxConfiguratorTest(util.NginxTest):
filep = self.config.parser.abs_path('sites-enabled/example.com')
self.config.parser.add_server_directives(
filep, set(['.example.com', 'example.*']),
[['listen', '5001 ssl']])
[['listen', '5001 ssl']],
replace=False)
self.config.save()
# pylint: disable=protected-access
parsed = self.config.parser._parse_files(filep, override=True)
self.assertEqual([[['server'], [['listen', '5001 ssl'],
self.assertEqual([[['server'], [
['listen', '69.50.225.155:9000'],
['listen', '127.0.0.1'],
['server_name', '.example.com'],
['server_name', 'example.*']]]],
['server_name', 'example.*'],
['listen', '5001 ssl']
]]],
parsed[0])
def test_choose_vhost(self):
@@ -91,12 +111,26 @@ class NginxConfiguratorTest(util.NginxTest):
'test.www.example.com': foo_conf,
'abc.www.foo.com': foo_conf,
'www.bar.co.uk': localhost_conf}
conf_path = {'localhost': "etc_nginx/nginx.conf",
'alias': "etc_nginx/nginx.conf",
'example.com': "etc_nginx/sites-enabled/example.com",
'example.com.uk.test': "etc_nginx/sites-enabled/example.com",
'www.example.com': "etc_nginx/sites-enabled/example.com",
'test.www.example.com': "etc_nginx/foo.conf",
'abc.www.foo.com': "etc_nginx/foo.conf",
'www.bar.co.uk': "etc_nginx/nginx.conf"}
bad_results = ['www.foo.com', 'example', 't.www.bar.co',
'69.255.225.155']
for name in results:
self.assertEqual(results[name],
self.config.choose_vhost(name).names)
vhost = self.config.choose_vhost(name)
path = os.path.relpath(vhost.filep, self.temp_dir)
self.assertEqual(results[name], vhost.names)
self.assertEqual(conf_path[name], path)
for name in bad_results:
self.assertEqual(set([name]), self.config.choose_vhost(name).names)
@@ -154,38 +188,36 @@ class NginxConfiguratorTest(util.NginxTest):
parsed_server_conf = util.filter_comments(self.config.parser.parsed[server_conf])
parsed_nginx_conf = util.filter_comments(self.config.parser.parsed[nginx_conf])
access_log = os.path.join(self.work_dir, "access.log")
error_log = os.path.join(self.work_dir, "error.log")
self.assertEqual([[['server'],
[['include', self.config.parser.loc["ssl_options"]],
['ssl_certificate_key', 'example/key.pem'],
['ssl_certificate', 'example/fullchain.pem'],
['error_log', error_log],
['access_log', access_log],
['listen', '5001 ssl'],
[
['listen', '69.50.225.155:9000'],
['listen', '127.0.0.1'],
['server_name', '.example.com'],
['server_name', 'example.*']]]],
['server_name', 'example.*'],
['listen', '5001 ssl'],
['ssl_certificate', 'example/fullchain.pem'],
['ssl_certificate_key', 'example/key.pem'],
['include', self.config.parser.loc["ssl_options"]]
]]],
parsed_example_conf)
self.assertEqual([['server_name', 'somename alias another.alias']],
parsed_server_conf)
self.assertTrue(util.contains_at_depth(parsed_nginx_conf,
[['server'],
[['include', self.config.parser.loc["ssl_options"]],
['ssl_certificate_key', '/etc/nginx/key.pem'],
['ssl_certificate', '/etc/nginx/fullchain.pem'],
['error_log', error_log],
['access_log', access_log],
['listen', '5001 ssl'],
['listen', '8000'],
['listen', 'somename:8080'],
['include', 'server.conf'],
[['location', '/'],
[['root', 'html'],
['index', 'index.html index.htm']]]]],
2))
self.assertTrue(util.contains_at_depth(
parsed_nginx_conf,
[['server'],
[
['listen', '8000'],
['listen', 'somename:8080'],
['include', 'server.conf'],
[['location', '/'],
[['root', 'html'],
['index', 'index.html index.htm']]],
['listen', '5001 ssl'],
['ssl_certificate', '/etc/nginx/fullchain.pem'],
['ssl_certificate_key', '/etc/nginx/key.pem'],
['include', self.config.parser.loc["ssl_options"]]]],
2))
def test_get_all_certs_keys(self):
nginx_conf = self.config.parser.abs_path('nginx.conf')
@@ -297,19 +329,19 @@ class NginxConfiguratorTest(util.NginxTest):
mocked = mock_popen()
mocked.communicate.return_value = ('', '')
mocked.returncode = 0
self.assertTrue(self.config.restart())
self.config.restart()
@mock.patch("letsencrypt_nginx.configurator.subprocess.Popen")
def test_nginx_restart_fail(self, mock_popen):
mocked = mock_popen()
mocked.communicate.return_value = ('', '')
mocked.returncode = 1
self.assertFalse(self.config.restart())
self.assertRaises(errors.MisconfigurationError, self.config.restart)
@mock.patch("letsencrypt_nginx.configurator.subprocess.Popen")
def test_no_nginx_start(self, mock_popen):
mock_popen.side_effect = OSError("Can't find program")
self.assertRaises(SystemExit, self.config.restart)
self.assertRaises(errors.MisconfigurationError, self.config.restart)
@mock.patch("letsencrypt_nginx.configurator.subprocess.Popen")
def test_config_test(self, mock_popen):
@@ -330,6 +362,17 @@ class NginxConfiguratorTest(util.NginxTest):
OpenSSL.crypto.load_privatekey(
OpenSSL.crypto.FILETYPE_PEM, key_file.read())
def test_redirect_enhance(self):
expected = [
['if', '($scheme != "https")'],
[['return', '301 https://$host$request_uri']]
]
example_conf = self.config.parser.abs_path('sites-enabled/example.com')
self.config.enhance("www.example.com", "redirect")
generated_conf = self.config.parser.parsed[example_conf]
self.assertTrue(util.contains_at_depth(generated_conf, expected, 2))
if __name__ == "__main__":
unittest.main() # pragma: no cover
@@ -127,7 +127,8 @@ class NginxParserTest(util.NginxTest):
set(['localhost',
r'~^(www\.)?(example|bar)\.']),
[['foo', 'bar'], ['ssl_certificate',
'/etc/ssl/cert.pem']])
'/etc/ssl/cert.pem']],
replace=False)
ssl_re = re.compile(r'\n\s+ssl_certificate /etc/ssl/cert.pem')
dump = nginxparser.dumps(nparser.parsed[nparser.abs_path('nginx.conf')])
self.assertEqual(1, len(re.findall(ssl_re, dump)))
@@ -136,12 +137,15 @@ class NginxParserTest(util.NginxTest):
names = set(['alias', 'another.alias', 'somename'])
nparser.add_server_directives(server_conf, names,
[['foo', 'bar'], ['ssl_certificate',
'/etc/ssl/cert2.pem']])
nparser.add_server_directives(server_conf, names, [['foo', 'bar']])
'/etc/ssl/cert2.pem']],
replace=False)
nparser.add_server_directives(server_conf, names, [['foo', 'bar']],
replace=False)
self.assertEqual(nparser.parsed[server_conf],
[['ssl_certificate', '/etc/ssl/cert2.pem'],
[['server_name', 'somename alias another.alias'],
['foo', 'bar'],
['server_name', 'somename alias another.alias']])
['ssl_certificate', '/etc/ssl/cert2.pem']
])
def test_add_http_directives(self):
nparser = parser.NginxParser(self.config_path, self.ssl_options)
@@ -165,17 +169,19 @@ class NginxParserTest(util.NginxTest):
target = set(['.example.com', 'example.*'])
filep = nparser.abs_path('sites-enabled/example.com')
nparser.add_server_directives(
filep, target, [['server_name', 'foo bar']], True)
filep, target, [['server_name', 'foobar.com']], replace=True)
self.assertEqual(
nparser.parsed[filep],
[[['server'], [['listen', '69.50.225.155:9000'],
['listen', '127.0.0.1'],
['server_name', 'foo bar'],
['server_name', 'foo bar']]]])
['server_name', 'foobar.com'],
['server_name', 'example.*'],
]]])
self.assertRaises(errors.MisconfigurationError,
nparser.add_server_directives,
filep, set(['foo', 'bar']),
[['ssl_certificate', 'cert.pem']], True)
filep, set(['foobar.com', 'example.*']),
[['ssl_certificate', 'cert.pem']],
replace=True)
def test_get_best_match(self):
target_name = 'www.eff.org'
@@ -217,7 +223,8 @@ class NginxParserTest(util.NginxTest):
set(['.example.com', 'example.*']),
[['ssl_certificate', 'foo.pem'],
['ssl_certificate_key', 'bar.key'],
['listen', '443 ssl']])
['listen', '443 ssl']],
replace=False)
c_k = nparser.get_all_certs_keys()
self.assertEqual(set([('foo.pem', 'bar.key', filep)]), c_k)
@@ -20,13 +20,14 @@ events {
}
http {
# Set an array of temp and cache file options that will otherwise default to
# Set an array of temp, cache and log file options that will otherwise default to
# restricted locations accessible only to root.
client_body_temp_path $root/client_body;
fastcgi_temp_path $root/fastcgi_temp;
proxy_temp_path $root/proxy_temp;
#scgi_temp_path $root/scgi_temp;
#uwsgi_temp_path $root/uwsgi_temp;
access_log $root/error.log;
# This should be turned off in a Virtualbox VM, as it can cause some
# interesting issues with data corruption in delivered files.
@@ -54,9 +55,6 @@ http {
root $root/webroot;
access_log $root/access.log;
error_log $root/error.log;
location / {
# First attempt to serve request as file, then as directory, then fall
# back to index.html.
-6
View File
@@ -40,7 +40,6 @@ install_requires = [
'pyrfc3339',
'python2-pythondialog>=3.2.2rc1', # Debian squeeze support, cf. #280
'pytz',
'requests',
'setuptools', # pkg_resources
'six',
'zope.component',
@@ -61,11 +60,6 @@ else:
'mock',
])
if sys.version_info < (2, 7, 9):
# For secure SSL connexion with Python 2.7 (InsecurePlatformWarning)
install_requires.append('ndg-httpsclient')
install_requires.append('pyasn1')
dev_extras = [
# Pin astroid==1.3.5, pylint==1.4.2 as a workaround for #289
'astroid==1.3.5',
@@ -1,28 +0,0 @@
#!/bin/bash
# A hackish script to see if the client is behaving as expected
# with each of the "passing" conf files.
# TODO presently this requires interaction and human judgement to
# assess, but it should be automated
export EA=/etc/apache2/
TESTDIR="`dirname $0`"
LEROOT="`realpath \"$TESTDIR/../../\"`"
cd $TESTDIR/passing
function CleanupExit() {
echo control c, exiting tests...
if [ "$f" != "" ] ; then
sudo rm /etc/apache2/sites-{enabled,available}/"$f"
fi
exit 1
}
trap CleanupExit INT
for f in *.conf ; do
echo testing "$f"
sudo cp "$f" "$EA"/sites-available/
sudo ln -s "$EA/sites-available/$f" "$EA/sites-enabled/$f"
sudo "$LEROOT"/venv/bin/letsencrypt --apache certonly -t
sudo rm /etc/apache2/sites-{enabled,available}/"$f"
done
@@ -1,7 +0,0 @@
Modules required to parse these conf files:
ssl
rewrite
macro
wsgi
deflate
+2 -1
View File
@@ -75,7 +75,7 @@ parser.add_argument('--saveinstances',
action='store_true',
help="don't kill EC2 instances after run, useful for debugging")
parser.add_argument('--alt_pip',
default='https://certainly.isnot.org/pip/',
default='',
help="server from which to pull candidate release packages")
cl_args = parser.parse_args()
@@ -478,6 +478,7 @@ for outq in outputs:
results_file.close()
if not cl_args.saveinstances:
print('Logs in ', LOGDIR)
print('Terminating EC2 Instances and Cleaning Dangling EBS Volumes')
boulder_server.terminate()
terminate_and_clean(instances)
+18 -4
View File
@@ -8,6 +8,7 @@ then
CONFFILE=/etc/apache2/sites-available/000-default.conf
sudo apt-get update
sudo apt-get -y --no-upgrade install apache2 #curl
sudo apt-get -y install realpath # needed for test-apache-conf
# For apache 2.4, set up ServerName
sudo sed -i '/ServerName/ s/#ServerName/ServerName/' $CONFFILE
sudo sed -i '/ServerName/ s/www.example.com/'$PUBLIC_HOSTNAME'/' $CONFFILE
@@ -36,7 +37,21 @@ fi
# run letsencrypt-apache2 via letsencrypt-auto
cd letsencrypt
./letsencrypt-auto -v --debug --text --agree-dev-preview --agree-tos \
export SUDO=sudo
if [ -f /etc/debian_version ] ; then
echo "Bootstrapping dependencies for Debian-based OSes..."
$SUDO bootstrap/_deb_common.sh
elif [ -f /etc/redhat-release ] ; then
echo "Bootstrapping dependencies for RedHat-based OSes..."
$SUDO bootstrap/_rpm_common.sh
else
echo "Dont have bootstrapping for this OS!"
exit 1
fi
bootstrap/dev/venv.sh
sudo venv/bin/letsencrypt -v --debug --text --agree-dev-preview --agree-tos \
--renew-by-default --redirect --register-unsafely-without-email \
--domain $PUBLIC_HOSTNAME --server $BOULDER_URL
if [ $? -ne 0 ] ; then
@@ -44,8 +59,7 @@ if [ $? -ne 0 ] ; then
fi
if [ "$OS_TYPE" = "ubuntu" ] ; then
export LETSENCRYPT="$HOME/.local/share/letsencrypt/bin/letsencrypt"
tests/apache-conf-files/hackish-apache-test --debian-modules
venv/bin/tox -e apacheconftest
else
echo Not running hackish apache tests on $OS_TYPE
fi
@@ -56,5 +70,5 @@ fi
# return error if any of the subtests failed
if [ "$FAIL" = 1 ] ; then
return 1
exit 1
fi
+8
View File
@@ -67,3 +67,11 @@ commands =
pylint --rcfile=.pylintrc letsencrypt-nginx/letsencrypt_nginx
pylint --rcfile=.pylintrc letsencrypt-compatibility-test/letsencrypt_compatibility_test
pylint --rcfile=.pylintrc letshelp-letsencrypt/letshelp_letsencrypt
[testenv:apacheconftest]
#basepython = python2.7
setenv =
LETSENCRYPT=/home/travis/build/letsencrypt/letsencrypt/.tox/apacheconftest/bin/letsencrypt
commands =
pip install -e acme -e .[dev] -e letsencrypt-apache -e letsencrypt-nginx -e letsencrypt-compatibility-test -e letshelp-letsencrypt
sudo ./letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/apache-conf-test --debian-modules