Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/certbot/certbot.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.rst2
-rw-r--r--acme/acme/client.py6
-rw-r--r--acme/examples/example_client.py3
-rwxr-xr-xbootstrap/_rpm_common.sh2
-rw-r--r--docs/using.rst45
-rw-r--r--letsencrypt-apache/letsencrypt_apache/augeas_lens/httpd.aug27
-rw-r--r--letsencrypt-apache/letsencrypt_apache/configurator.py54
-rw-r--r--letsencrypt-apache/letsencrypt_apache/tests/configurator_test.py33
-rw-r--r--letsencrypt-apache/letsencrypt_apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/conf-available/bad_conf_file.conf2
-rw-r--r--letsencrypt/cli.py249
-rw-r--r--letsencrypt/configuration.py41
-rw-r--r--letsencrypt/constants.py3
-rw-r--r--letsencrypt/display/ops.py22
-rw-r--r--letsencrypt/le_util.py38
-rw-r--r--letsencrypt/plugins/standalone.py19
-rw-r--r--letsencrypt/plugins/standalone_test.py18
-rw-r--r--letsencrypt/plugins/webroot.py59
-rw-r--r--letsencrypt/plugins/webroot_test.py31
-rw-r--r--letsencrypt/storage.py20
-rw-r--r--letsencrypt/tests/cli_test.py25
-rw-r--r--letsencrypt/tests/display/ops_test.py12
-rw-r--r--letsencrypt/tests/testdata/cli.ini1
-rw-r--r--tests/apache-conf-files/failing/missing-double-quote-1724.conf52
-rwxr-xr-xtests/apache-conf-files/hackish-apache-test28
-rw-r--r--tests/apache-conf-files/passing/1626-1531.conf37
-rw-r--r--tests/apache-conf-files/passing/README.modules2
-rw-r--r--tests/apache-conf-files/passing/drupal-errordocument-arg-1724.conf116
-rw-r--r--tests/apache-conf-files/passing/drupal-htaccess-1531.conf (renamed from tests/apache-conf-files/failing/drupal-htaccess-1531.conf)0
-rw-r--r--tests/apache-conf-files/passing/example-1755.conf36
-rw-r--r--tests/apache-conf-files/passing/missing-quote-1724.conf52
-rw-r--r--tests/apache-conf-files/passing/sslrequire-wordlist-1827.htaccess1
-rw-r--r--tests/apache-conf-files/passing/two-blocks-one-line-1693.conf (renamed from tests/apache-conf-files/failing/two-blocks-one-line-1693.conf)0
32 files changed, 825 insertions, 211 deletions
diff --git a/README.rst b/README.rst
index d1f5d3428..57908e90f 100644
--- a/README.rst
+++ b/README.rst
@@ -163,5 +163,5 @@ Current Features
* Free and Open Source Software, made with Python.
-.. _Freenode: https://freenode.net
+.. _Freenode: https://webchat.freenode.net?channels=%23letsencrypt
.. _client-dev: https://groups.google.com/a/letsencrypt.org/forum/#!forum/client-dev
diff --git a/acme/acme/client.py b/acme/acme/client.py
index 08d476783..c3e28ef47 100644
--- a/acme/acme/client.py
+++ b/acme/acme/client.py
@@ -20,7 +20,9 @@ from acme import messages
logger = logging.getLogger(__name__)
-# Python does not validate certificates by default before version 2.7.9
+# Prior to Python 2.7.9 the stdlib SSL module did not allow a user to configure
+# many important security related options. On these platforms we use PyOpenSSL
+# for SSL, which does allow these options to be configured.
# https://urllib3.readthedocs.org/en/latest/security.html#insecureplatformwarning
if sys.version_info < (2, 7, 9): # pragma: no cover
requests.packages.urllib3.contrib.pyopenssl.inject_into_urllib3()
@@ -338,7 +340,7 @@ class Client(object): # pylint: disable=too-many-instance-attributes
`PollError` with non-empty ``waiting`` is raised.
:returns: ``(cert, updated_authzrs)`` `tuple` where ``cert`` is
- the issued certificate (`.messages.CertificateResource.),
+ the issued certificate (`.messages.CertificateResource`),
and ``updated_authzrs`` is a `tuple` consisting of updated
Authorization Resources (`.AuthorizationResource`) as
present in the responses from server, and in the same order
diff --git a/acme/examples/example_client.py b/acme/examples/example_client.py
index b4b5ad010..f6b0329f5 100644
--- a/acme/examples/example_client.py
+++ b/acme/examples/example_client.py
@@ -28,8 +28,7 @@ acme = client.Client(DIRECTORY_URL, key)
regr = acme.register()
logging.info('Auto-accepting TOS: %s', regr.terms_of_service)
-acme.update_registration(regr.update(
- body=regr.body.update(agreement=regr.terms_of_service)))
+acme.agree_to_tos(regr)
logging.debug(regr)
authzr = acme.request_challenges(
diff --git a/bootstrap/_rpm_common.sh b/bootstrap/_rpm_common.sh
index b975da444..411d7bd92 100755
--- a/bootstrap/_rpm_common.sh
+++ b/bootstrap/_rpm_common.sh
@@ -2,7 +2,7 @@
# Tested with:
# - Fedora 22, 23 (x64)
-# - Centos 7 (x64: onD igitalOcean droplet)
+# - Centos 7 (x64: on DigitalOcean droplet)
if type dnf 2>/dev/null
then
diff --git a/docs/using.rst b/docs/using.rst
index 6e15d2cf2..1423d6eba 100644
--- a/docs/using.rst
+++ b/docs/using.rst
@@ -31,16 +31,21 @@ Firstly, please `install Git`_ and run the following commands:
.. _`install Git`: https://git-scm.com/book/en/v2/Getting-Started-Installing-Git
+.. note:: On RedHat/CentOS 6 you will need to enable the EPEL_
+ repository before install.
+
+.. _EPEL: http://fedoraproject.org/wiki/EPEL
+
To install and run the client you just need to type:
.. code-block:: shell
./letsencrypt-auto
-.. note:: On RedHat/CentOS 6 you will need to enable the EPEL_
- repository before install.
-
-.. _EPEL: http://fedoraproject.org/wiki/EPEL
+.. hint:: During the beta phase, Let's Encrypt enforces strict rate limits on
+ the number of certificates issued for one domain. It is recommended to
+ initially use the test server via `--test-cert` until you get the desired
+ certificates.
Throughout the documentation, whenever you see references to
``letsencrypt`` script/binary, you can substitute in
@@ -58,8 +63,8 @@ or for full help, type:
``letsencrypt-auto`` is the recommended method of running the Let's Encrypt
-client beta releases on systems that don't have a packaged version. Debian
-experimental, Arch linux and FreeBSD now have native packages, so on those
+client beta releases on systems that don't have a packaged version. Debian,
+Arch linux and FreeBSD now have native packages, so on those
systems you can just install ``letsencrypt`` (and perhaps
``letsencrypt-apache``). If you'd like to run the latest copy from Git, or
run your own locally modified copy of the client, follow the instructions in
@@ -173,10 +178,10 @@ Renewal
In order to renew certificates simply call the ``letsencrypt`` (or
letsencrypt-auto_) again, and use the same values when prompted. You
can automate it slightly by passing necessary flags on the CLI (see
-`--help all`), or even further using the :ref:`config-file`. The
-``--renew-by-default`` flag may be helpful for automating renewal. If
-you're sure that UI doesn't prompt for any details you can add the
-command to ``crontab`` (make it less than every 90 days to avoid
+`--help all`), or even further using the :ref:`config-file`. The
+``--renew-by-default`` flag may be helpful for automating renewal. If
+you're sure that UI doesn't prompt for any details you can add the
+command to ``crontab`` (make it less than every 90 days to avoid
problems, say every month).
Please note that the CA will send notification emails to the address
@@ -224,21 +229,23 @@ The following files are available:
``cert.pem``
Server certificate only.
- This is what Apache needs for `SSLCertificateFile
+ This is what Apache < 2.4.8 needs for `SSLCertificateFile
<https://httpd.apache.org/docs/2.4/mod/mod_ssl.html#sslcertificatefile>`_.
``chain.pem``
All certificates that need to be served by the browser **excluding**
server certificate, i.e. root and intermediate certificates only.
- This is what Apache needs for `SSLCertificateChainFile
+ This is what Apache < 2.4.8 needs for `SSLCertificateChainFile
<https://httpd.apache.org/docs/2.4/mod/mod_ssl.html#sslcertificatechainfile>`_.
``fullchain.pem``
All certificates, **including** server certificate. This is
concatenation of ``chain.pem`` and ``cert.pem``.
- This is what nginx needs for `ssl_certificate
+ This is what Apache >= 2.4.8 needs for `SSLCertificateFile
+ <https://httpd.apache.org/docs/2.4/mod/mod_ssl.html#sslcertificatefile>`_,
+ and what nginx needs for `ssl_certificate
<http://nginx.org/en/docs/http/ngx_http_ssl_module.html#ssl_certificate>`_.
@@ -287,7 +294,7 @@ get support on our `forums <https://community.letsencrypt.org>`_.
If you find a bug in the software, please do report it in our `issue
tracker
<https://github.com/letsencrypt/letsencrypt/issues>`_. Remember to
-give us us as much information as possible:
+give us as much information as possible:
- copy and paste exact command line used and the output (though mind
that the latter might include some personally identifiable
@@ -350,20 +357,20 @@ Operating System Packages
sudo pacman -S letsencrypt letsencrypt-apache
-**Debian Experimental**
+**Debian**
-If you run Debian unstable, you can install experimental letsencrypt packages.
-Add the line ``deb http://ftp.us.debian.org/debian/ experimental main`` (or
-the equivalent for your country) to ``/etc/apt/sources.list``, then run
+If you run Debian Stretch or Debian Sid, you can install letsencrypt packages.
.. code-block:: shell
sudo apt-get update
- sudo apt-get -t experimental install letsencrypt python-letsencrypt-apache
+ sudo apt-get install letsencrypt python-letsencrypt-apache
If you don't want to use the Apache plugin, you can ommit the
``python-letsencrypt-apache`` package.
+Packages for Debian Jessie are coming in the next few weeks.
+
**Other Operating Systems**
OS packaging is an ongoing effort. If you'd like to package
diff --git a/letsencrypt-apache/letsencrypt_apache/augeas_lens/httpd.aug b/letsencrypt-apache/letsencrypt_apache/augeas_lens/httpd.aug
index 30d8ca501..d665ea7a7 100644
--- a/letsencrypt-apache/letsencrypt_apache/augeas_lens/httpd.aug
+++ b/letsencrypt-apache/letsencrypt_apache/augeas_lens/httpd.aug
@@ -51,7 +51,7 @@ let sep_osp = Sep.opt_space
let sep_eq = del /[ \t]*=[ \t]*/ "="
let nmtoken = /[a-zA-Z:_][a-zA-Z0-9:_.-]*/
-let word = /[a-zA-Z][a-zA-Z0-9._-]*/
+let word = /[a-z][a-z0-9._-]*/i
let comment = Util.comment
let eol = Util.doseol
@@ -59,13 +59,18 @@ let empty = Util.empty_dos
let indent = Util.indent
(* borrowed from shellvars.aug *)
-let char_arg_dir = /([^\\ '"\t\r\n]|[^\\ '"\t\r\n][^ '"\t\r\n]*[^\\ '"\t\r\n])|\\\\"|\\\\'/
+let char_arg_dir = /([^\\ '"{\t\r\n]|[^ '"{\t\r\n]+[^\\ '"\t\r\n])|\\\\"|\\\\'/
let char_arg_sec = /[^ '"\t\r\n>]|\\\\"|\\\\'/
+let char_arg_wl = /([^\\ '"},\t\r\n]|[^ '"},\t\r\n]+[^\\ '"},\t\r\n])/
+
let cdot = /\\\\./
let cl = /\\\\\n/
let dquot =
let no_dquot = /[^"\\\r\n]/
in /"/ . (no_dquot|cdot|cl)* . /"/
+let dquot_msg =
+ let no_dquot = /([^ \t"\\\r\n]|[^"\\\r\n]+[^ \t"\\\r\n])/
+ in /"/ . (no_dquot|cdot|cl)*
let squot =
let no_squot = /[^'\\\r\n]/
in /'/ . (no_squot|cdot|cl)* . /'/
@@ -76,12 +81,24 @@ let comp = /[<>=]?=/
*****************************************************************)
let arg_dir = [ label "arg" . store (char_arg_dir+|dquot|squot) ]
+(* message argument starts with " but ends at EOL *)
+let arg_dir_msg = [ label "arg" . store dquot_msg ]
let arg_sec = [ label "arg" . store (char_arg_sec+|comp|dquot|squot) ]
+let arg_wl = [ label "arg" . store (char_arg_wl+|dquot|squot) ]
+
+(* comma-separated wordlist as permitted in the SSLRequire directive *)
+let arg_wordlist =
+ let wl_start = Util.del_str "{" in
+ let wl_end = Util.del_str "}" in
+ let wl_sep = del /[ \t]*,[ \t]*/ ", "
+ in [ label "wordlist" . wl_start . arg_wl . (wl_sep . arg_wl)* . wl_end ]
let argv (l:lens) = l . (sep_spc . l)*
-let directive = [ indent . label "directive" . store word .
- (sep_spc . argv arg_dir)? . eol ]
+let directive =
+ (* arg_dir_msg may be the last or only argument *)
+ let dir_args = (argv (arg_dir|arg_wordlist) . (sep_spc . arg_dir_msg)?) | arg_dir_msg
+ in [ indent . label "directive" . store word . (sep_spc . dir_args)? . eol ]
let section (body:lens) =
(* opt_eol includes empty lines *)
@@ -91,7 +108,7 @@ let section (body:lens) =
indent . dels "</" in
let kword = key word in
let dword = del word "a" in
- [ indent . dels "<" . square kword inner dword . del ">" ">" . eol ]
+ [ indent . dels "<" . square kword inner dword . del />[ \t\n\r]*/ ">\n" ]
let rec content = section (content|directive)
diff --git a/letsencrypt-apache/letsencrypt_apache/configurator.py b/letsencrypt-apache/letsencrypt_apache/configurator.py
index 98b0b8820..0b40a7e38 100644
--- a/letsencrypt-apache/letsencrypt_apache/configurator.py
+++ b/letsencrypt-apache/letsencrypt_apache/configurator.py
@@ -93,7 +93,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
add("enmod", default=constants.CLI_DEFAULTS["enmod"],
help="Path to the Apache 'a2enmod' binary.")
add("dismod", default=constants.CLI_DEFAULTS["dismod"],
- help="Path to the Apache 'a2enmod' binary.")
+ help="Path to the Apache 'a2dismod' binary.")
add("le-vhost-ext", default=constants.CLI_DEFAULTS["le_vhost_ext"],
help="SSL vhost configuration extension.")
add("server-root", default=constants.CLI_DEFAULTS["server_root"],
@@ -120,7 +120,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
self.version = version
self.vhosts = None
self._enhance_func = {"redirect": self._enable_redirect,
- "ensure-http-header": self._set_http_header}
+ "ensure-http-header": self._set_http_header}
@property
def mod_ssl_conf(self):
@@ -545,21 +545,43 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
# Check for Listen <port>
# Note: This could be made to also look for ip:443 combo
- if not self.parser.find_dir("Listen", port):
- logger.debug("No Listen %s directive found. Setting the "
- "Apache Server to Listen on port %s", port, port)
-
- if port == "443":
- args = [port]
+ 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"]
+ for listen in listens:
+ # For any listen statement, check if the machine also listens on Port 443.
+ # If not, add such a listen statement.
+ if len(listen.split(":")) == 1:
+ # Its listening to all interfaces
+ if port not in listens:
+ if port == "443":
+ args = [port]
+ else:
+ # Non-standard ports should specify https protocol
+ args = [port, "https"]
+ self.parser.add_dir_to_ifmodssl(
+ parser.get_aug_path(
+ self.parser.loc["listen"]), "Listen", args)
+ self.save_notes += "Added Listen %s directive to %s\n" % (
+ port, self.parser.loc["listen"])
+ listens.append(port)
else:
- # Non-standard ports should specify https protocol
- args = [port, "https"]
-
- self.parser.add_dir_to_ifmodssl(
- parser.get_aug_path(
- self.parser.loc["listen"]), "Listen", args)
- self.save_notes += "Added Listen %s directive to %s\n" % (
- port, self.parser.loc["listen"])
+ # The Listen statement specifies an ip
+ _, ip = listen[::-1].split(":", 1)
+ ip = ip[::-1]
+ if "%s:%s" % (ip, port) not in listens:
+ if port == "443":
+ args = ["%s:%s" % (ip, port)]
+ else:
+ # Non-standard ports should specify https protocol
+ args = ["%s:%s" % (ip, port), "https"]
+ self.parser.add_dir_to_ifmodssl(
+ parser.get_aug_path(
+ self.parser.loc["listen"]), "Listen", args)
+ self.save_notes += "Added Listen %s:%s directive to %s\n" % (
+ ip, port, self.parser.loc["listen"])
+ listens.append("%s:%s" % (ip, port))
def make_addrs_sni_ready(self, addrs):
"""Checks to see if the server is ready for SNI challenges.
diff --git a/letsencrypt-apache/letsencrypt_apache/tests/configurator_test.py b/letsencrypt-apache/letsencrypt_apache/tests/configurator_test.py
index fcccfaae2..991704144 100644
--- a/letsencrypt-apache/letsencrypt_apache/tests/configurator_test.py
+++ b/letsencrypt-apache/letsencrypt_apache/tests/configurator_test.py
@@ -391,6 +391,39 @@ class TwoVhost80Test(util.ApacheTest):
self.assertEqual(mock_add_dir.call_count, 2)
+ def test_prepare_server_https_named_listen(self):
+ mock_find = mock.Mock()
+ mock_find.return_value = ["test1", "test2", "test3"]
+ mock_get = mock.Mock()
+ mock_get.side_effect = ["1.2.3.4:80", "[::1]:80", "1.1.1.1: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, 2)
+
+ # Check argument to new Listen statements
+ self.assertEqual(mock_add_dir.call_args_list[0][0][2], ["1.2.3.4:443"])
+ self.assertEqual(mock_add_dir.call_args_list[1][0][2], ["[::1]:443"])
+
+ # Reset return lists and inputs
+ mock_add_dir.reset_mock()
+ mock_get.side_effect = ["1.2.3.4:80", "[::1]:80", "1.1.1.1:443"]
+
+ # Test
+ self.config.prepare_server_https("8080", temp=True)
+ self.assertEqual(mock_add_dir.call_count, 3)
+ self.assertEqual(mock_add_dir.call_args_list[0][0][2], ["1.2.3.4:8080", "https"])
+ 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_make_vhost_ssl(self):
ssl_vhost = self.config.make_vhost_ssl(self.vh_truth[0])
diff --git a/letsencrypt-apache/letsencrypt_apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/conf-available/bad_conf_file.conf b/letsencrypt-apache/letsencrypt_apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/conf-available/bad_conf_file.conf
index 1aad6a9f4..8e9178803 100644
--- a/letsencrypt-apache/letsencrypt_apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/conf-available/bad_conf_file.conf
+++ b/letsencrypt-apache/letsencrypt_apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/conf-available/bad_conf_file.conf
@@ -1,5 +1,3 @@
<VirtualHost 1.1.1.1>
ServerName invalid.net
-
-</virtualHost>
diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py
index 61cde7a3f..29519d430 100644
--- a/letsencrypt/cli.py
+++ b/letsencrypt/cli.py
@@ -207,76 +207,131 @@ def _find_duplicative_certs(config, domains):
if candidate_names == set(domains):
identical_names_cert = candidate_lineage
elif candidate_names.issubset(set(domains)):
- subset_names_cert = candidate_lineage
+ # This logic finds and returns the largest subset-names cert
+ # in the case where there are several available.
+ if subset_names_cert is None:
+ subset_names_cert = candidate_lineage
+ elif len(candidate_names) > len(subset_names_cert.names()):
+ subset_names_cert = candidate_lineage
return identical_names_cert, subset_names_cert
def _treat_as_renewal(config, domains):
- """Determine whether or not the call should be treated as a renewal.
+ """Determine whether there are duplicated names and how to handle them.
- :returns: RenewableCert or None if renewal shouldn't occur.
- :rtype: :class:`.storage.RenewableCert`
+ :returns: Two-element tuple containing desired new-certificate behavior as
+ a string token ("reinstall", "renew", or "newcert"), plus either
+ a RenewableCert instance or None if renewal shouldn't occur.
:raises .Error: If the user would like to rerun the client again.
"""
- renewal = False
-
# Considering the possibility that the requested certificate is
# related to an existing certificate. (config.duplicate, which
# is set with --duplicate, skips all of this logic and forces any
# kind of certificate to be obtained with renewal = False.)
- if not config.duplicate:
- ident_names_cert, subset_names_cert = _find_duplicative_certs(
- config, domains)
- # I am not sure whether that correctly reads the systemwide
- # configuration file.
- question = None
- if ident_names_cert is not None:
- question = (
- "You have an existing certificate that contains exactly the "
- "same domains you requested (ref: {0}){br}{br}Do you want to "
- "renew and replace this certificate with a newly-issued one?"
- ).format(ident_names_cert.configfile.filename, br=os.linesep)
- elif subset_names_cert is not None:
- question = (
- "You have an existing certificate that contains a portion of "
- "the domains you requested (ref: {0}){br}{br}It contains these "
- "names: {1}{br}{br}You requested these names for the new "
- "certificate: {2}.{br}{br}Do you want to replace this existing "
- "certificate with the new certificate?"
- ).format(subset_names_cert.configfile.filename,
- ", ".join(subset_names_cert.names()),
- ", ".join(domains),
- br=os.linesep)
- if question is None:
- # We aren't in a duplicative-names situation at all, so we don't
- # have to tell or ask the user anything about this.
- pass
- elif config.renew_by_default or zope.component.getUtility(
- interfaces.IDisplay).yesno(question, "Replace", "Cancel"):
- renewal = True
- else:
- reporter_util = zope.component.getUtility(interfaces.IReporter)
- reporter_util.add_message(
- "To obtain a new certificate that {0} an existing certificate "
- "in its domain-name coverage, you must use the --duplicate "
- "option.{br}{br}For example:{br}{br}{1} --duplicate {2}".format(
- "duplicates" if ident_names_cert is not None else
- "overlaps with",
- sys.argv[0], " ".join(sys.argv[1:]),
- br=os.linesep
- ),
- reporter_util.HIGH_PRIORITY)
- raise errors.Error(
- "User did not use proper CLI and would like "
- "to reinvoke the client.")
-
- if renewal:
- return ident_names_cert if ident_names_cert is not None else subset_names_cert
-
- return None
+ if config.duplicate:
+ return "newcert", None
+ # TODO: Also address superset case
+ ident_names_cert, subset_names_cert = _find_duplicative_certs(config, domains)
+ # XXX ^ schoen is not sure whether that correctly reads the systemwide
+ # configuration file.
+ if ident_names_cert is None and subset_names_cert is None:
+ return "newcert", None
+
+ if ident_names_cert is not None:
+ return _handle_identical_cert_request(config, ident_names_cert)
+ elif subset_names_cert is not None:
+ return _handle_subset_cert_request(config, domains, subset_names_cert)
+
+def _handle_identical_cert_request(config, cert):
+ """Figure out what to do if a cert has the same names as a perviously obtained one
+
+ :param storage.RenewableCert cert:
+
+ :returns: Tuple of (string, cert_or_None) as per _treat_as_renewal
+ :rtype: tuple
+
+ """
+ if config.renew_by_default:
+ logger.info("Auto-renewal forced with --renew-by-default...")
+ return "renew", cert
+ if cert.should_autorenew(interactive=True):
+ logger.info("Cert is due for renewal, auto-renewing...")
+ return "renew", cert
+ if config.reinstall:
+ # Set with --reinstall, force an identical certificate to be
+ # reinstalled without further prompting.
+ return "reinstall", cert
+
+ question = (
+ "You have an existing certificate that contains exactly the same "
+ "domains you requested and isn't close to expiry."
+ "{br}(ref: {0}){br}{br}What would you like to do?"
+ ).format(cert.configfile.filename, br=os.linesep)
+
+ if config.verb == "run":
+ keep_opt = "Attempt to reinstall this existing certificate"
+ elif config.verb == "certonly":
+ keep_opt = "Keep the existing certificate for now"
+ choices = [keep_opt,
+ "Renew & replace the cert (limit ~5 per 7 days)",
+ "Cancel this operation and do nothing"]
+
+ display = zope.component.getUtility(interfaces.IDisplay)
+ response = display.menu(question, choices, "OK", "Cancel")
+ if response[0] == "cancel" or response[1] == 2:
+ # TODO: Add notification related to command-line options for
+ # skipping the menu for this case.
+ raise errors.Error(
+ "User chose to cancel the operation and may "
+ "reinvoke the client.")
+ elif response[1] == 0:
+ return "reinstall", cert
+ elif response[1] == 1:
+ return "renew", cert
+ else:
+ assert False, "This is impossible"
+
+def _handle_subset_cert_request(config, domains, cert):
+ """Figure out what to do if a previous cert had a subset of the names now requested
+
+ :param storage.RenewableCert cert:
+
+ :returns: Tuple of (string, cert_or_None) as per _treat_as_renewal
+ :rtype: tuple
+
+ """
+ existing = ", ".join(cert.names())
+ question = (
+ "You have an existing certificate that contains a portion of "
+ "the domains you requested (ref: {0}){br}{br}It contains these "
+ "names: {1}{br}{br}You requested these names for the new "
+ "certificate: {2}.{br}{br}Do you want to expand and replace this existing "
+ "certificate with the new certificate?"
+ ).format(cert.configfile.filename,
+ existing,
+ ", ".join(domains),
+ br=os.linesep)
+ if config.expand or config.renew_by_default or zope.component.getUtility(
+ interfaces.IDisplay).yesno(question, "Expand", "Cancel"):
+ return "renew", cert
+ else:
+ reporter_util = zope.component.getUtility(interfaces.IReporter)
+ reporter_util.add_message(
+ "To obtain a new certificate that contains these names without "
+ "replacing your existing certificate for {0}, you must use the "
+ "--duplicate option.{br}{br}"
+ "For example:{br}{br}{1} --duplicate {2}".format(
+ existing,
+ sys.argv[0], " ".join(sys.argv[1:]),
+ br=os.linesep
+ ),
+ reporter_util.HIGH_PRIORITY)
+ raise errors.Error(
+ "User chose to cancel the operation and may "
+ "reinvoke the client.")
def _report_new_cert(cert_path, fullchain_path):
@@ -316,10 +371,21 @@ def _suggest_donate():
def _auth_from_domains(le_client, config, domains):
"""Authenticate and enroll certificate."""
- # Note: This can raise errors... caught above us though.
- lineage = _treat_as_renewal(config, domains)
-
- if lineage is not None:
+ # Note: This can raise errors... caught above us though. This is now
+ # a three-way case: reinstall (which results in a no-op here because
+ # although there is a relevant lineage, we don't do anything to it
+ # inside this function -- we don't obtain a new certificate), renew
+ # (which results in treating the request as a renewal), or newcert
+ # (which results in treating the request as a new certificate request).
+
+ action, lineage = _treat_as_renewal(config, domains)
+ if action == "reinstall":
+ # The lineage already exists; allow the caller to try installing
+ # it without getting a new certificate at all.
+ return lineage
+ elif action == "renew":
+ original_server = lineage.configuration["renewalparams"]["server"]
+ _avoid_invalidating_lineage(config, lineage, original_server)
# TODO: schoen wishes to reuse key - discussion
# https://github.com/letsencrypt/letsencrypt/pull/777/files#r40498574
new_certr, new_chain, new_key, _ = le_client.obtain_certificate(domains)
@@ -333,7 +399,7 @@ def _auth_from_domains(le_client, config, domains):
# TODO: Check return value of save_successor
# TODO: Also update lineage renewal config with any relevant
# configuration values from this attempt? <- Absolutely (jdkasten)
- else:
+ elif action == "newcert":
# TREAT AS NEW REQUEST
lineage = le_client.obtain_and_enroll_certificate(domains)
if not lineage:
@@ -343,6 +409,27 @@ def _auth_from_domains(le_client, config, domains):
return lineage
+def _avoid_invalidating_lineage(config, lineage, original_server):
+ "Do not renew a valid cert with one from a staging server!"
+ def _is_staging(srv):
+ return srv == constants.STAGING_URI or "staging" in srv
+
+ # Some lineages may have begun with --staging, but then had production certs
+ # added to them
+ latest_cert = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM,
+ open(lineage.cert).read())
+ # all our test certs are from happy hacker fake CA, though maybe one day
+ # we should test more methodically
+ now_valid = not "fake" in repr(latest_cert.get_issuer()).lower()
+
+ if _is_staging(config.server):
+ if not _is_staging(original_server) or now_valid:
+ if not config.break_my_certs:
+ names = ", ".join(lineage.names())
+ raise errors.Error(
+ "You've asked to renew/replace a seemingly valid certificate with "
+ "a test certificate (domains: {0}). We will not do that "
+ "unless you use the --break-my-certs flag!".format(names))
def set_configurator(previously, now):
"""
@@ -487,7 +574,6 @@ def run(args, config, plugins): # pylint: disable=too-many-branches,too-many-lo
def obtain_cert(args, config, plugins):
"""Authenticate & obtain cert, but do not install it."""
-
if args.domains and args.csr is not None:
# TODO: --csr could have a priority, when --domains is
# supplied, check if CSR matches given domains?
@@ -697,6 +783,15 @@ class HelpfulArgumentParser(object):
"""
parsed_args = self.parser.parse_args(self.args)
parsed_args.func = self.VERBS[self.verb]
+ parsed_args.verb = self.verb
+
+ # Do any post-parsing homework here
+
+ # argparse seemingly isn't flexible enough to give us this behaviour easily...
+ if parsed_args.staging:
+ if parsed_args.server not in (flag_default("server"), constants.STAGING_URI):
+ raise errors.Error("--server value conflicts with --staging")
+ parsed_args.server = constants.STAGING_URI
return parsed_args
@@ -871,27 +966,38 @@ def prepare_and_parse_args(plugins, args):
help="Domain names to apply. For multiple domains you can use "
"multiple -d flags or enter a comma separated list of domains "
"as a parameter.")
- helpful.add(
- None, "--duplicate", dest="duplicate", action="store_true",
- help="Allow getting a certificate that duplicates an existing one")
-
helpful.add_group(
"automation",
description="Arguments for automating execution & other tweaks")
helpful.add(
+ "automation", "--keep-until-expiring", "--keep", "--reinstall",
+ dest="reinstall", action="store_true",
+ help="If the requested cert matches an existing cert, always keep the "
+ "existing one until it is due for renewal (for the "
+ "'run' subcommand this means reinstall the existing cert)")
+ helpful.add(
+ "automation", "--expand", action="store_true",
+ help="If an existing cert covers some subset of the requested names, "
+ "always expand and replace it with the additional names.")
+ helpful.add(
"automation", "--version", action="version",
version="%(prog)s {0}".format(letsencrypt.__version__),
help="show program's version number and exit")
helpful.add(
"automation", "--renew-by-default", action="store_true",
help="Select renewal by default when domains are a superset of a "
- "previously attained cert")
+ "previously attained cert (often --keep-until-expiring is "
+ "more appropriate). Implies --expand.")
helpful.add(
"automation", "--agree-tos", dest="tos", action="store_true",
help="Agree to the Let's Encrypt Subscriber Agreement")
helpful.add(
"automation", "--account", metavar="ACCOUNT_ID",
help="Account ID to use")
+ helpful.add(
+ "automation", "--duplicate", dest="duplicate", action="store_true",
+ help="Allow making a certificate lineage that duplicates an existing one "
+ "(both can be renewed in parallel)")
helpful.add_group(
"testing", description="The following flags are meant for "
@@ -912,7 +1018,10 @@ def prepare_and_parse_args(plugins, args):
helpful.add(
"testing", "--http-01-port", type=int, dest="http01_port",
default=flag_default("http01_port"), help=config_help("http01_port"))
-
+ helpful.add(
+ "testing", "--break-my-certs", action="store_true",
+ help="Be willing to replace or renew valid certs with invalid "
+ "(testing/staging) certs")
helpful.add_group(
"security", description="Security parameters & server settings")
helpful.add(
@@ -1039,6 +1148,10 @@ def _paths_parser(helpful):
help="Logs directory.")
add("paths", "--server", default=flag_default("server"),
help=config_help("server"))
+ # overwrites server, handled in HelpfulArgumentParser.parse_args()
+ add("testing", "--test-cert", "--staging", action='store_true', dest='staging',
+ help='Use the staging server to obtain test (invalid) certs; equivalent'
+ ' to --server ' + constants.STAGING_URI)
def _plugins_parsing(helpful, plugins):
diff --git a/letsencrypt/configuration.py b/letsencrypt/configuration.py
index 69778f5f0..afd5edbe4 100644
--- a/letsencrypt/configuration.py
+++ b/letsencrypt/configuration.py
@@ -1,13 +1,13 @@
"""Let's Encrypt user-supplied configuration."""
import os
import urlparse
-import re
import zope.interface
from letsencrypt import constants
from letsencrypt import errors
from letsencrypt import interfaces
+from letsencrypt import le_util
class NamespaceConfig(object):
@@ -123,40 +123,5 @@ def check_config_sanity(config):
# Domain checks
if config.namespace.domains is not None:
- _check_config_domain_sanity(config.namespace.domains)
-
-
-def _check_config_domain_sanity(domains):
- """Helper method for check_config_sanity which validates
- domain flag values and errors out if the requirements are not met.
-
- :param domains: List of domains
- :type domains: `list` of `string`
- :raises ConfigurationError: for invalid domains and cases where Let's
- Encrypt currently will not issue certificates
-
- """
- # Check if there's a wildcard domain
- if any(d.startswith("*.") for d in domains):
- raise errors.ConfigurationError(
- "Wildcard domains are not supported")
- # Punycode
- if any("xn--" in d for d in domains):
- raise errors.ConfigurationError(
- "Punycode domains are not supported")
-
- # Unicode
- try:
- for domain in domains:
- domain.encode('ascii')
- except UnicodeDecodeError:
- raise errors.ConfigurationError(
- "Internationalized domain names are not supported")
-
- # FQDN checks from
- # http://www.mkyong.com/regular-expressions/domain-name-regular-expression-example/
- # Characters used, domain parts < 63 chars, tld > 1 < 64 chars
- # first and last char is not "-"
- fqdn = re.compile("^((?!-)[A-Za-z0-9-]{1,63}(?<!-)\\.)+[A-Za-z]{2,63}$")
- if any(True for d in domains if not fqdn.match(d)):
- raise errors.ConfigurationError("Requested domain is not a FQDN")
+ for domain in config.namespace.domains:
+ le_util.check_domain_sanity(domain)
diff --git a/letsencrypt/constants.py b/letsencrypt/constants.py
index 40155abd7..a1dccd1ea 100644
--- a/letsencrypt/constants.py
+++ b/letsencrypt/constants.py
@@ -30,8 +30,9 @@ CLI_DEFAULTS = dict(
auth_chain_path="./chain.pem",
strict_permissions=False,
)
-"""Defaults for CLI flags and `.IConfig` attributes."""
+STAGING_URI = "https://acme-staging.api.letsencrypt.org/directory"
+"""Defaults for CLI flags and `.IConfig` attributes."""
RENEWER_DEFAULTS = dict(
renewer_enabled="yes",
diff --git a/letsencrypt/display/ops.py b/letsencrypt/display/ops.py
index 038ad6fdc..102dbe3a0 100644
--- a/letsencrypt/display/ops.py
+++ b/letsencrypt/display/ops.py
@@ -4,6 +4,7 @@ import os
import zope.component
+from letsencrypt import errors
from letsencrypt import interfaces
from letsencrypt import le_util
from letsencrypt.display import util as display_util
@@ -186,7 +187,8 @@ def choose_names(installer):
logger.debug("No installer, picking names manually")
return _choose_names_manually()
- names = list(installer.get_all_names())
+ domains = list(installer.get_all_names())
+ names = get_valid_domains(domains)
if not names:
manual = util(interfaces.IDisplay).yesno(
@@ -207,6 +209,22 @@ def choose_names(installer):
else:
return []
+def get_valid_domains(domains):
+ """Helper method for choose_names that implements basic checks
+ on domain names
+
+ :param list domains: Domain names to validate
+ :return: List of valid domains
+ :rtype: list
+ """
+ valid_domains = []
+ for domain in domains:
+ try:
+ le_util.check_domain_sanity(domain)
+ valid_domains.append(domain)
+ except errors.ConfigurationError:
+ continue
+ return valid_domains
def _filter_names(names):
"""Determine which names the user would like to select from a list.
@@ -245,7 +263,7 @@ def success_installation(domains):
"""
util(interfaces.IDisplay).notification(
- "Congratulations! You have successfully enabled {0}!{1}{1}"
+ "Congratulations! You have successfully enabled {0}{1}{1}"
"You should test your configuration at:{1}{2}".format(
_gen_https_names(domains),
os.linesep,
diff --git a/letsencrypt/le_util.py b/letsencrypt/le_util.py
index 7869fc9a5..64295a80f 100644
--- a/letsencrypt/le_util.py
+++ b/letsencrypt/le_util.py
@@ -10,6 +10,8 @@ import stat
import subprocess
import sys
+import configargparse
+
from letsencrypt import errors
@@ -278,5 +280,41 @@ def add_deprecated_argument(add_argument, argument_name, nargs):
sys.stderr.write(
"Use of {0} is deprecated.\n".format(option_string))
+ configargparse.ACTION_TYPES_THAT_DONT_NEED_A_VALUE.add(ShowWarning)
add_argument(argument_name, action=ShowWarning,
help=argparse.SUPPRESS, nargs=nargs)
+
+
+def check_domain_sanity(domain):
+ """Method which validates domain value and errors out if
+ the requirements are not met.
+
+ :param domain: Domain to check
+ :type domains: `string`
+ :raises ConfigurationError: for invalid domains and cases where Let's
+ Encrypt currently will not issue certificates
+
+ """
+ # Check if there's a wildcard domain
+ if domain.startswith("*."):
+ raise errors.ConfigurationError(
+ "Wildcard domains are not supported")
+ # Punycode
+ if "xn--" in domain:
+ raise errors.ConfigurationError(
+ "Punycode domains are not presently supported")
+
+ # Unicode
+ try:
+ domain.encode('ascii')
+ except UnicodeDecodeError:
+ raise errors.ConfigurationError(
+ "Internationalized domain names are not presently supported")
+
+ # FQDN checks from
+ # http://www.mkyong.com/regular-expressions/domain-name-regular-expression-example/
+ # Characters used, domain parts < 63 chars, tld > 1 < 64 chars
+ # first and last char is not "-"
+ fqdn = re.compile("^((?!-)[A-Za-z0-9-]{1,63}(?<!-)\\.)+[A-Za-z]{2,63}$")
+ if not fqdn.match(domain):
+ raise errors.ConfigurationError("Requested domain is not a FQDN")
diff --git a/letsencrypt/plugins/standalone.py b/letsencrypt/plugins/standalone.py
index 8b8612fd1..4319e51f9 100644
--- a/letsencrypt/plugins/standalone.py
+++ b/letsencrypt/plugins/standalone.py
@@ -2,7 +2,6 @@
import argparse
import collections
import logging
-import random
import socket
import threading
@@ -108,7 +107,7 @@ class ServerManager(object):
in six.iteritems(self._instances))
-SUPPORTED_CHALLENGES = set([challenges.TLSSNI01, challenges.HTTP01])
+SUPPORTED_CHALLENGES = [challenges.TLSSNI01, challenges.HTTP01]
def supported_challenges_validator(data):
@@ -166,16 +165,16 @@ class Authenticator(common.Plugin):
@classmethod
def add_parser_arguments(cls, add):
- add("supported-challenges", help="Supported challenges, "
- "order preferences are randomly chosen.",
- type=supported_challenges_validator, default=",".join(
- sorted(chall.typ for chall in SUPPORTED_CHALLENGES)))
+ add("supported-challenges",
+ help="Supported challenges. Preferred in the order they are listed.",
+ type=supported_challenges_validator,
+ default=",".join(chall.typ for chall in SUPPORTED_CHALLENGES))
@property
def supported_challenges(self):
"""Challenges supported by this plugin."""
- return set(challenges.Challenge.TYPES[name] for name in
- self.conf("supported-challenges").split(","))
+ return [challenges.Challenge.TYPES[name] for name in
+ self.conf("supported-challenges").split(",")]
@property
def _necessary_ports(self):
@@ -198,9 +197,7 @@ class Authenticator(common.Plugin):
def get_chall_pref(self, domain):
# pylint: disable=unused-argument,missing-docstring
- chall_pref = list(self.supported_challenges)
- random.shuffle(chall_pref) # 50% for each challenge
- return chall_pref
+ return self.supported_challenges
def perform(self, achalls): # pylint: disable=missing-docstring
if any(util.already_listening(port) for port in self._necessary_ports):
diff --git a/letsencrypt/plugins/standalone_test.py b/letsencrypt/plugins/standalone_test.py
index 26a040c2e..1e39dee57 100644
--- a/letsencrypt/plugins/standalone_test.py
+++ b/letsencrypt/plugins/standalone_test.py
@@ -98,17 +98,27 @@ class AuthenticatorTest(unittest.TestCase):
def test_supported_challenges(self):
self.assertEqual(self.auth.supported_challenges,
- set([challenges.TLSSNI01, challenges.HTTP01]))
+ [challenges.TLSSNI01, challenges.HTTP01])
+
+ def test_supported_challenges_configured(self):
+ self.config.standalone_supported_challenges = "tls-sni-01"
+ self.assertEqual(self.auth.supported_challenges,
+ [challenges.TLSSNI01])
def test_more_info(self):
self.assertTrue(isinstance(self.auth.more_info(), six.string_types))
def test_get_chall_pref(self):
- self.assertEqual(set(self.auth.get_chall_pref(domain=None)),
- set([challenges.TLSSNI01, challenges.HTTP01]))
+ self.assertEqual(self.auth.get_chall_pref(domain=None),
+ [challenges.TLSSNI01, challenges.HTTP01])
+
+ def test_get_chall_pref_configured(self):
+ self.config.standalone_supported_challenges = "tls-sni-01"
+ self.assertEqual(self.auth.get_chall_pref(domain=None),
+ [challenges.TLSSNI01])
@mock.patch("letsencrypt.plugins.standalone.util")
- def test_perform_alredy_listening(self, mock_util):
+ def test_perform_already_listening(self, mock_util):
for chall, port in ((challenges.TLSSNI01.typ, 1234),
(challenges.HTTP01.typ, 4321)):
mock_util.already_listening.return_value = True
diff --git a/letsencrypt/plugins/webroot.py b/letsencrypt/plugins/webroot.py
index 0b81d45b5..0679bc349 100644
--- a/letsencrypt/plugins/webroot.py
+++ b/letsencrypt/plugins/webroot.py
@@ -2,7 +2,6 @@
import errno
import logging
import os
-import stat
import zope.interface
@@ -59,24 +58,38 @@ to serve all files under specified web root ({0})."""
logger.debug("Creating root challenges validation dir at %s",
self.full_roots[name])
+
+ # Change the permissions to be writable (GH #1389)
+ # Umask is used instead of chmod to ensure the client can also
+ # run as non-root (GH #1795)
+ old_umask = os.umask(0o022)
+
try:
- os.makedirs(self.full_roots[name])
- # Set permissions as parent directory (GH #1389)
- # We don't use the parameters in makedirs because it
- # may not always work
+ # This is coupled with the "umask" call above because
+ # os.makedirs's "mode" parameter may not always work:
# https://stackoverflow.com/questions/5231901/permission-problems-when-creating-a-dir-with-os-makedirs-python
- stat_path = os.stat(path)
- filemode = stat.S_IMODE(stat_path.st_mode)
- os.chmod(self.full_roots[name], filemode)
- # Set owner and group, too
- os.chown(self.full_roots[name], stat_path.st_uid,
- stat_path.st_gid)
+ os.makedirs(self.full_roots[name], 0o0755)
+
+ # Set owner as parent directory if possible
+ try:
+ stat_path = os.stat(path)
+ os.chown(self.full_roots[name], stat_path.st_uid,
+ stat_path.st_gid)
+ except OSError as exception:
+ if exception.errno == errno.EACCES:
+ logger.debug("Insufficient permissions to change owner and uid - ignoring")
+ else:
+ raise errors.PluginError(
+ "Couldn't create root for {0} http-01 "
+ "challenge responses: {1}", name, exception)
except OSError as exception:
if exception.errno != errno.EEXIST:
raise errors.PluginError(
"Couldn't create root for {0} http-01 "
"challenge responses: {1}", name, exception)
+ finally:
+ os.umask(old_umask)
def perform(self, achalls): # pylint: disable=missing-docstring
assert self.full_roots, "Webroot plugin appears to be missing webroot map"
@@ -87,26 +100,26 @@ to serve all files under specified web root ({0})."""
path = self.full_roots[achall.domain]
except IndexError:
raise errors.PluginError("Missing --webroot-path for domain: {1}"
- .format(achall.domain))
+ .format(achall.domain))
if not os.path.exists(path):
raise errors.PluginError("Mysteriously missing path {0} for domain: {1}"
- .format(path, achall.domain))
+ .format(path, achall.domain))
return os.path.join(path, achall.chall.encode("token"))
def _perform_single(self, achall):
response, validation = achall.response_and_validation()
+
path = self._path_for_achall(achall)
logger.debug("Attempting to save validation to %s", path)
- with open(path, "w") as validation_file:
- validation_file.write(validation.encode())
-
- # Set permissions as parent directory (GH #1389)
- parent_path = self.full_roots[achall.domain]
- stat_parent_path = os.stat(parent_path)
- filemode = stat.S_IMODE(stat_parent_path.st_mode)
- # Remove execution bit (not needed for this file)
- os.chmod(path, filemode & ~stat.S_IEXEC)
- os.chown(path, stat_parent_path.st_uid, stat_parent_path.st_gid)
+
+ # Change permissions to be world-readable, owner-writable (GH #1795)
+ old_umask = os.umask(0o022)
+
+ try:
+ with open(path, "w") as validation_file:
+ validation_file.write(validation.encode())
+ finally:
+ os.umask(old_umask)
return response
diff --git a/letsencrypt/plugins/webroot_test.py b/letsencrypt/plugins/webroot_test.py
index e7f96b50d..9f5b6bba8 100644
--- a/letsencrypt/plugins/webroot_test.py
+++ b/letsencrypt/plugins/webroot_test.py
@@ -1,9 +1,10 @@
"""Tests for letsencrypt.plugins.webroot."""
+import errno
import os
import shutil
+import stat
import tempfile
import unittest
-import stat
import mock
@@ -35,7 +36,6 @@ class AuthenticatorTest(unittest.TestCase):
self.config = mock.MagicMock(webroot_path=self.path,
webroot_map={"thing.com": self.path})
self.auth = Authenticator(self.config, "webroot")
- self.auth.prepare()
def tearDown(self):
shutil.rmtree(self.path)
@@ -48,7 +48,7 @@ class AuthenticatorTest(unittest.TestCase):
def test_add_parser_arguments(self):
add = mock.MagicMock()
self.auth.add_parser_arguments(add)
- self.assertEqual(0, add.call_count) # became 0 when we moved the args to cli.py!
+ self.assertEqual(0, add.call_count) # args moved to cli.py!
def test_prepare_bad_root(self):
self.config.webroot_path = os.path.join(self.path, "null")
@@ -70,17 +70,33 @@ class AuthenticatorTest(unittest.TestCase):
self.assertRaises(errors.PluginError, self.auth.prepare)
os.chmod(self.path, 0o700)
+ @mock.patch("letsencrypt.plugins.webroot.os.chown")
+ def test_failed_chown_eacces(self, mock_chown):
+ mock_chown.side_effect = OSError(errno.EACCES, "msg")
+ self.auth.prepare() # exception caught and logged
+
+ @mock.patch("letsencrypt.plugins.webroot.os.chown")
+ def test_failed_chown_not_eacces(self, mock_chown):
+ mock_chown.side_effect = OSError()
+ self.assertRaises(errors.PluginError, self.auth.prepare)
+
def test_prepare_permissions(self):
+ self.auth.prepare()
# Remove exec bit from permission check, so that it
# matches the file
self.auth.perform([self.achall])
- parent_permissions = (stat.S_IMODE(os.stat(self.path).st_mode) &
- ~stat.S_IEXEC)
+ path_permissions = stat.S_IMODE(os.stat(self.validation_path).st_mode)
+ self.assertEqual(path_permissions, 0o644)
- actual_permissions = stat.S_IMODE(os.stat(self.validation_path).st_mode)
+ # Check permissions of the directories
+
+ for dirpath, dirnames, _ in os.walk(self.path):
+ for directory in dirnames:
+ full_path = os.path.join(dirpath, directory)
+ dir_permissions = stat.S_IMODE(os.stat(full_path).st_mode)
+ self.assertEqual(dir_permissions, 0o755)
- self.assertEqual(parent_permissions, actual_permissions)
parent_gid = os.stat(self.path).st_gid
parent_uid = os.stat(self.path).st_uid
@@ -88,6 +104,7 @@ class AuthenticatorTest(unittest.TestCase):
self.assertEqual(os.stat(self.validation_path).st_uid, parent_uid)
def test_perform_cleanup(self):
+ self.auth.prepare()
responses = self.auth.perform([self.achall])
self.assertEqual(1, len(responses))
self.assertTrue(os.path.exists(self.validation_path))
diff --git a/letsencrypt/storage.py b/letsencrypt/storage.py
index 7e2802b14..3b2b548b0 100644
--- a/letsencrypt/storage.py
+++ b/letsencrypt/storage.py
@@ -471,7 +471,7 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes
return ("autodeploy" not in self.configuration or
self.configuration.as_bool("autodeploy"))
- def should_autodeploy(self):
+ def should_autodeploy(self, interactive=False):
"""Should this lineage now automatically deploy a newer version?
This is a policy question and does not only depend on whether
@@ -480,12 +480,16 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes
exists, and whether the time interval for autodeployment has
been reached.)
+ :param bool interactive: set to True to examine the question
+ regardless of whether the renewal configuration allows
+ automated deployment (for interactive use). Default False.
+
:returns: whether the lineage now ought to autodeploy an
existing newer cert version
:rtype: bool
"""
- if self.autodeployment_is_enabled():
+ if interactive or self.autodeployment_is_enabled():
if self.has_pending_deployment():
interval = self.configuration.get("deploy_before_expiry",
"5 days")
@@ -529,7 +533,7 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes
return ("autorenew" not in self.configuration or
self.configuration.as_bool("autorenew"))
- def should_autorenew(self):
+ def should_autorenew(self, interactive=False):
"""Should we now try to autorenew the most recent cert version?
This is a policy question and does not only depend on whether
@@ -540,12 +544,16 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes
Note that this examines the numerically most recent cert version,
not the currently deployed version.
+ :param bool interactive: set to True to examine the question
+ regardless of whether the renewal configuration allows
+ automated renewal (for interactive use). Default False.
+
:returns: whether an attempt should now be made to autorenew the
most current cert version in this lineage
:rtype: bool
"""
- if self.autorenewal_is_enabled():
+ if interactive or self.autorenewal_is_enabled():
# Consider whether to attempt to autorenew this cert now
# Renewals on the basis of revocation
@@ -559,8 +567,8 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes
"cert", self.latest_common_version()))
now = pytz.UTC.fromutc(datetime.datetime.utcnow())
if expiry < add_time_interval(now, interval):
- logger.debug("Should renew, certificate "
- "has been expired since %s.",
+ logger.debug("Should renew, less than %s before certificate "
+ "expiry %s.", interval,
expiry.strftime("%Y-%m-%d %H:%M:%S %Z"))
return True
return False
diff --git a/letsencrypt/tests/cli_test.py b/letsencrypt/tests/cli_test.py
index 462d37a87..ccf16f5b5 100644
--- a/letsencrypt/tests/cli_test.py
+++ b/letsencrypt/tests/cli_test.py
@@ -15,6 +15,7 @@ from acme import jose
from letsencrypt import account
from letsencrypt import cli
from letsencrypt import configuration
+from letsencrypt import constants
from letsencrypt import crypto_util
from letsencrypt import errors
from letsencrypt import le_util
@@ -343,6 +344,19 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods
namespace = cli.prepare_and_parse_args(plugins, long_args)
self.assertEqual(namespace.domains, ['example.com', 'another.net'])
+ def test_parse_server(self):
+ plugins = disco.PluginsRegistry.find_all()
+ short_args = ['--server', 'example.com']
+ namespace = cli.prepare_and_parse_args(plugins, short_args)
+ self.assertEqual(namespace.server, 'example.com')
+
+ short_args = ['--staging']
+ namespace = cli.prepare_and_parse_args(plugins, short_args)
+ self.assertEqual(namespace.server, constants.STAGING_URI)
+
+ short_args = ['--staging', '--server', 'example.com']
+ self.assertRaises(errors.Error, cli.prepare_and_parse_args, plugins, short_args)
+
def test_parse_webroot(self):
plugins = disco.PluginsRegistry.find_all()
webroot_args = ['--webroot', '-w', '/var/www/example',
@@ -389,7 +403,7 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods
def _certonly_new_request_common(self, mock_client):
with mock.patch('letsencrypt.cli._treat_as_renewal') as mock_renewal:
- mock_renewal.return_value = None
+ mock_renewal.return_value = ("newcert", None)
with mock.patch('letsencrypt.cli._init_le_client') as mock_init:
mock_init.return_value = mock_client
self._call(['-d', 'foo.bar', '-a', 'standalone', 'certonly'])
@@ -399,13 +413,13 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods
@mock.patch('letsencrypt.cli._treat_as_renewal')
@mock.patch('letsencrypt.cli._init_le_client')
def test_certonly_renewal(self, mock_init, mock_renewal, mock_get_utility, _suggest):
- cert_path = '/etc/letsencrypt/live/foo.bar/cert.pem'
+ cert_path = 'letsencrypt/tests/testdata/cert.pem'
chain_path = '/etc/letsencrypt/live/foo.bar/fullchain.pem'
mock_lineage = mock.MagicMock(cert=cert_path, fullchain=chain_path)
mock_cert = mock.MagicMock(body='body')
mock_key = mock.MagicMock(pem='pem_key')
- mock_renewal.return_value = mock_lineage
+ mock_renewal.return_value = ("renew", mock_lineage)
mock_client = mock.MagicMock()
mock_client.obtain_certificate.return_value = (mock_cert, 'chain',
mock_key, 'csr')
@@ -537,6 +551,11 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods
self.assertEqual(path, os.path.abspath(path))
self.assertEqual(contents, test_contents)
+ def test_agree_dev_preview_config(self):
+ with MockedVerb('run') as mocked_run:
+ self._call(['-c', test_util.vector_path('cli.ini')])
+ self.assertTrue(mocked_run.called)
+
class DetermineAccountTest(unittest.TestCase):
"""Tests for letsencrypt.cli._determine_account."""
diff --git a/letsencrypt/tests/display/ops_test.py b/letsencrypt/tests/display/ops_test.py
index b0b905c33..30183b955 100644
--- a/letsencrypt/tests/display/ops_test.py
+++ b/letsencrypt/tests/display/ops_test.py
@@ -1,3 +1,4 @@
+# coding=utf-8
"""Test letsencrypt.display.ops."""
import os
import sys
@@ -385,6 +386,17 @@ class ChooseNamesTest(unittest.TestCase):
self.assertEqual(self._call(self.mock_install), [])
+ def test_get_valid_domains(self):
+ from letsencrypt.display.ops import get_valid_domains
+ all_valid = ["example.com", "second.example.com",
+ "also.example.com"]
+ all_invalid = ["xn--ls8h.tld", "*.wildcard.com", "notFQDN",
+ "uniçodé.com"]
+ two_valid = ["example.com", "xn--ls8h.tld", "also.example.com"]
+ self.assertEqual(get_valid_domains(all_valid), all_valid)
+ self.assertEqual(get_valid_domains(all_invalid), [])
+ self.assertEqual(len(get_valid_domains(two_valid)), 2)
+
class SuccessInstallationTest(unittest.TestCase):
# pylint: disable=too-few-public-methods
diff --git a/letsencrypt/tests/testdata/cli.ini b/letsencrypt/tests/testdata/cli.ini
new file mode 100644
index 000000000..8ef506071
--- /dev/null
+++ b/letsencrypt/tests/testdata/cli.ini
@@ -0,0 +1 @@
+agree-dev-preview = True
diff --git a/tests/apache-conf-files/failing/missing-double-quote-1724.conf b/tests/apache-conf-files/failing/missing-double-quote-1724.conf
new file mode 100644
index 000000000..7d97b23d0
--- /dev/null
+++ b/tests/apache-conf-files/failing/missing-double-quote-1724.conf
@@ -0,0 +1,52 @@
+<VirtualHost *:443>
+ ServerAdmin webmaster@localhost
+ ServerAlias www.example.com
+ ServerName example.com
+ DocumentRoot /var/www/example.com/www/
+ SSLEngine on
+
+ SSLProtocol all -SSLv2 -SSLv3
+ SSLCipherSuite "EECDH+ECDSA+AESGCM EECDH+aRSA+AESGCM EECDH+ECDSA+SHA384 EECDH+ECDSA+SHA256 EECDH+aRS$
+ SSLCertificateFile /etc/ssl/certs/ssl-cert-snakeoil.pem
+ SSLCertificateKeyFile /etc/ssl/private/ssl-cert-snakeoil.key
+
+ <Directory />
+ Options FollowSymLinks
+ AllowOverride All
+ </Directory>
+ <Directory /var/www/example.com/www>
+ Options Indexes FollowSymLinks MultiViews
+ AllowOverride All
+ Order allow,deny
+ allow from all
+ # This directive allows us to have apache2's default start page
+ # in /apache2-default/, but still have / go to the right place
+ </Directory>
+
+ ScriptAlias /cgi-bin/ /usr/lib/cgi-bin/
+ <Directory "/usr/lib/cgi-bin">
+ AllowOverride None
+ Options +ExecCGI -MultiViews +SymLinksIfOwnerMatch
+ Order allow,deny
+ Allow from all
+ </Directory>
+
+ ErrorLog /var/log/apache2/error.log
+
+ # Possible values include: debug, info, notice, warn, error, crit,
+ # alert, emerg.
+ LogLevel warn
+
+ CustomLog /var/log/apache2/access.log combined
+ ServerSignature On
+
+ Alias /apache_doc/ "/usr/share/doc/"
+ <Directory "/usr/share/doc/">
+ Options Indexes MultiViews FollowSymLinks
+ AllowOverride None
+ Order deny,allow
+ Deny from all
+ Allow from 127.0.0.0/255.0.0.0 ::1/128
+ </Directory>
+
+</VirtualHost>
diff --git a/tests/apache-conf-files/hackish-apache-test b/tests/apache-conf-files/hackish-apache-test
new file mode 100755
index 000000000..c6663551e
--- /dev/null
+++ b/tests/apache-conf-files/hackish-apache-test
@@ -0,0 +1,28 @@
+#!/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
diff --git a/tests/apache-conf-files/passing/1626-1531.conf b/tests/apache-conf-files/passing/1626-1531.conf
new file mode 100644
index 000000000..1622a57df
--- /dev/null
+++ b/tests/apache-conf-files/passing/1626-1531.conf
@@ -0,0 +1,37 @@
+<VirtualHost *:80>
+ ServerAdmin denver@ossguy.com
+ ServerName c-beta.ossguy.com
+
+ Alias /robots.txt /home/denver/www/c-beta.ossguy.com/static/robots.txt
+ Alias /favicon.ico /home/denver/www/c-beta.ossguy.com/static/favicon.ico
+
+ AliasMatch /(.*\.css) /home/denver/www/c-beta.ossguy.com/static/$1
+ AliasMatch /(.*\.js) /home/denver/www/c-beta.ossguy.com/static/$1
+ AliasMatch /(.*\.png) /home/denver/www/c-beta.ossguy.com/static/$1
+ AliasMatch /(.*\.gif) /home/denver/www/c-beta.ossguy.com/static/$1
+ AliasMatch /(.*\.jpg) /home/denver/www/c-beta.ossguy.com/static/$1
+
+ WSGIScriptAlias / /home/denver/www/c-beta.ossguy.com/django.wsgi
+ WSGIDaemonProcess c-beta-ossguy user=www-data group=www-data home=/var/www processes=5 threads=10 maximum-requests=1000 umask=0007 display-name=c-beta-ossguy
+ WSGIProcessGroup c-beta-ossguy
+ WSGIApplicationGroup %{GLOBAL}
+
+ DocumentRoot /home/denver/www/c-beta.ossguy.com/static
+
+ <Directory /home/denver/www/c-beta.ossguy.com/static>
+ Options -Indexes +FollowSymLinks -MultiViews
+ Require all granted
+ AllowOverride None
+ </Directory>
+
+ <Directory /home/denver/www/c-beta.ossguy.com/static/source>
+ Options +Indexes +FollowSymLinks -MultiViews
+ Require all granted
+ AllowOverride None
+ </Directory>
+
+ # Custom log file locations
+ LogLevel warn
+ ErrorLog /tmp/error.log
+ CustomLog /tmp/access.log combined
+</VirtualHost>
diff --git a/tests/apache-conf-files/passing/README.modules b/tests/apache-conf-files/passing/README.modules
index 9c5853061..7edbd3e84 100644
--- a/tests/apache-conf-files/passing/README.modules
+++ b/tests/apache-conf-files/passing/README.modules
@@ -3,3 +3,5 @@ Modules required to parse these conf files:
ssl
rewrite
macro
+wsgi
+deflate
diff --git a/tests/apache-conf-files/passing/drupal-errordocument-arg-1724.conf b/tests/apache-conf-files/passing/drupal-errordocument-arg-1724.conf
new file mode 100644
index 000000000..4733ffa4a
--- /dev/null
+++ b/tests/apache-conf-files/passing/drupal-errordocument-arg-1724.conf
@@ -0,0 +1,116 @@
+#
+# Apache/PHP/Drupal settings:
+#
+
+# Protect files and directories from prying eyes.
+<FilesMatch "\.(engine|inc|info|install|make|module|profile|test|po|sh|.*sql|theme|tpl(\.php)?|xtmpl|svn-base)$|^(code-style\.pl|Entries.*|Repository|Root|Tag|Template|all-wcprops|entries|format)$">
+ Order allow,deny
+</FilesMatch>
+
+# Don't show directory listings for URLs which map to a directory.
+Options -Indexes
+
+# Follow symbolic links in this directory.
+Options +FollowSymLinks
+
+# Make Drupal handle any 404 errors.
+ErrorDocument 404 /index.php
+
+# Force simple error message for requests for non-existent favicon.ico.
+<Files favicon.ico>
+ # There is no end quote below, for compatibility with Apache 1.3.
+ ErrorDocument 404 "The requested file favicon.ico was not found.
+</Files>
+
+# Set the default handler.
+DirectoryIndex index.php
+
+# Override PHP settings. More in sites/default/settings.php
+# but the following cannot be changed at runtime.
+
+# PHP 4, Apache 1.
+<IfModule mod_php4.c>
+ php_value magic_quotes_gpc 0
+ php_value register_globals 0
+ php_value session.auto_start 0
+ php_value mbstring.http_input pass
+ php_value mbstring.http_output pass
+ php_value mbstring.encoding_translation 0
+</IfModule>
+
+# PHP 4, Apache 2.
+<IfModule sapi_apache2.c>
+ php_value magic_quotes_gpc 0
+ php_value register_globals 0
+ php_value session.auto_start 0
+ php_value mbstring.http_input pass
+ php_value mbstring.http_output pass
+ php_value mbstring.encoding_translation 0
+</IfModule>
+
+# PHP 5, Apache 1 and 2.
+<IfModule mod_php5.c>
+ php_value magic_quotes_gpc 0
+ php_value register_globals 0
+ php_value session.auto_start 0
+ php_value mbstring.http_input pass
+ php_value mbstring.http_output pass
+ php_value mbstring.encoding_translation 0
+</IfModule>
+
+# Requires mod_expires to be enabled.
+<IfModule mod_expires.c>
+ # Enable expirations.
+ ExpiresActive On
+
+ # Cache all files for 2 weeks after access (A).
+ ExpiresDefault A1209600
+
+ <FilesMatch \.php$>
+ # Do not allow PHP scripts to be cached unless they explicitly send cache
+ # headers themselves. Otherwise all scripts would have to overwrite the
+ # headers set by mod_expires if they want another caching behavior. This may
+ # fail if an error occurs early in the bootstrap process, and it may cause
+ # problems if a non-Drupal PHP file is installed in a subdirectory.
+ ExpiresActive Off
+ </FilesMatch>
+</IfModule>
+
+# Various rewrite rules.
+<IfModule mod_rewrite.c>
+ RewriteEngine on
+
+ # If your site can be accessed both with and without the 'www.' prefix, you
+ # can use one of the following settings to redirect users to your preferred
+ # URL, either WITH or WITHOUT the 'www.' prefix. Choose ONLY one option:
+ #
+ # To redirect all users to access the site WITH the 'www.' prefix,
+ # (http://example.com/... will be redirected to http://www.example.com/...)
+ # adapt and uncomment the following:
+ # RewriteCond %{HTTP_HOST} ^example\.com$ [NC]
+ # RewriteRule ^(.*)$ http://www.example.com/$1 [L,R=301]
+ #
+ # To redirect all users to access the site WITHOUT the 'www.' prefix,
+ # (http://www.example.com/... will be redirected to http://example.com/...)
+ # uncomment and adapt the following:
+ # RewriteCond %{HTTP_HOST} ^www\.example\.com$ [NC]
+ # RewriteRule ^(.*)$ http://example.com/$1 [L,R=301]
+
+ # Modify the RewriteBase if you are using Drupal in a subdirectory or in a
+ # VirtualDocumentRoot and the rewrite rules are not working properly.
+ # For example if your site is at http://example.com/drupal uncomment and
+ # modify the following line:
+ # RewriteBase /drupal
+ #
+ # If your site is running in a VirtualDocumentRoot at http://example.com/,
+ # uncomment the following line:
+ # RewriteBase /
+
+ # Rewrite URLs of the form 'x' to the form 'index.php?q=x'.
+ RewriteCond %{REQUEST_FILENAME} !-f
+ RewriteCond %{REQUEST_FILENAME} !-d
+ RewriteCond %{REQUEST_URI} !=/favicon.ico
+ RewriteRule ^(.*)$ index.php?q=$1 [L,QSA]
+</IfModule>
+
+# $Id$
diff --git a/tests/apache-conf-files/failing/drupal-htaccess-1531.conf b/tests/apache-conf-files/passing/drupal-htaccess-1531.conf
index a1aab7a39..a1aab7a39 100644
--- a/tests/apache-conf-files/failing/drupal-htaccess-1531.conf
+++ b/tests/apache-conf-files/passing/drupal-htaccess-1531.conf
diff --git a/tests/apache-conf-files/passing/example-1755.conf b/tests/apache-conf-files/passing/example-1755.conf
new file mode 100644
index 000000000..260029576
--- /dev/null
+++ b/tests/apache-conf-files/passing/example-1755.conf
@@ -0,0 +1,36 @@
+<VirtualHost *:80>
+ # The ServerName directive sets the request scheme, hostname and port that
+ # the server uses to identify itself. This is used when creating
+ # redirection URLs. In the context of virtual hosts, the ServerName
+ # specifies what hostname must appear in the request's Host: header to
+ # match this virtual host. For the default virtual host (this file) this
+ # value is not decisive as it is used as a last resort host regardless.
+ # However, you must set it for any further virtual host explicitly.
+ ServerName www.example.com
+ ServerAlias example.com
+SetOutputFilter DEFLATE
+# Do not attempt to compress the following extensions
+SetEnvIfNoCase Request_URI \
+\.(?:gif|jpe?g|png|swf|flv|zip|gz|tar|mp3|mp4|m4v)$ no-gzip dont-vary
+
+ ServerAdmin webmaster@localhost
+ DocumentRoot /var/www/proof
+
+ # Available loglevels: trace8, ..., trace1, debug, info, notice, warn,
+ # error, crit, alert, emerg.
+ # It is also possible to configure the loglevel for particular
+ # modules, e.g.
+ #LogLevel info ssl:warn
+
+ ErrorLog ${APACHE_LOG_DIR}/error.log
+ CustomLog ${APACHE_LOG_DIR}/access.log combined
+
+ # For most configuration files from conf-available/, which are
+ # enabled or disabled at a global level, it is possible to
+ # include a line for only one particular virtual host. For example the
+ # following line enables the CGI configuration for this host only
+ # after it has been globally disabled with "a2disconf".
+ #Include conf-available/serve-cgi-bin.conf
+</VirtualHost>
+
+# vim: syntax=apache ts=4 sw=4 sts=4 sr noet
diff --git a/tests/apache-conf-files/passing/missing-quote-1724.conf b/tests/apache-conf-files/passing/missing-quote-1724.conf
new file mode 100644
index 000000000..7d97b23d0
--- /dev/null
+++ b/tests/apache-conf-files/passing/missing-quote-1724.conf
@@ -0,0 +1,52 @@
+<VirtualHost *:443>
+ ServerAdmin webmaster@localhost
+ ServerAlias www.example.com
+ ServerName example.com
+ DocumentRoot /var/www/example.com/www/
+ SSLEngine on
+
+ SSLProtocol all -SSLv2 -SSLv3
+ SSLCipherSuite "EECDH+ECDSA+AESGCM EECDH+aRSA+AESGCM EECDH+ECDSA+SHA384 EECDH+ECDSA+SHA256 EECDH+aRS$
+ SSLCertificateFile /etc/ssl/certs/ssl-cert-snakeoil.pem
+ SSLCertificateKeyFile /etc/ssl/private/ssl-cert-snakeoil.key
+
+ <Directory />
+ Options FollowSymLinks
+ AllowOverride All
+ </Directory>
+ <Directory /var/www/example.com/www>
+ Options Indexes FollowSymLinks MultiViews
+ AllowOverride All
+ Order allow,deny
+ allow from all
+ # This directive allows us to have apache2's default start page
+ # in /apache2-default/, but still have / go to the right place
+ </Directory>
+
+ ScriptAlias /cgi-bin/ /usr/lib/cgi-bin/
+ <Directory "/usr/lib/cgi-bin">
+ AllowOverride None
+ Options +ExecCGI -MultiViews +SymLinksIfOwnerMatch
+ Order allow,deny
+ Allow from all
+ </Directory>
+
+ ErrorLog /var/log/apache2/error.log
+
+ # Possible values include: debug, info, notice, warn, error, crit,
+ # alert, emerg.
+ LogLevel warn
+
+ CustomLog /var/log/apache2/access.log combined
+ ServerSignature On
+
+ Alias /apache_doc/ "/usr/share/doc/"
+ <Directory "/usr/share/doc/">
+ Options Indexes MultiViews FollowSymLinks
+ AllowOverride None
+ Order deny,allow
+ Deny from all
+ Allow from 127.0.0.0/255.0.0.0 ::1/128
+ </Directory>
+
+</VirtualHost>
diff --git a/tests/apache-conf-files/passing/sslrequire-wordlist-1827.htaccess b/tests/apache-conf-files/passing/sslrequire-wordlist-1827.htaccess
new file mode 100644
index 000000000..1c06d5497
--- /dev/null
+++ b/tests/apache-conf-files/passing/sslrequire-wordlist-1827.htaccess
@@ -0,0 +1 @@
+SSLRequire %{SSL_CLIENT_S_DN_CN} in {"foo@bar.com", "bar@foo.com"}
diff --git a/tests/apache-conf-files/failing/two-blocks-one-line-1693.conf b/tests/apache-conf-files/passing/two-blocks-one-line-1693.conf
index 5d3cef423..5d3cef423 100644
--- a/tests/apache-conf-files/failing/two-blocks-one-line-1693.conf
+++ b/tests/apache-conf-files/passing/two-blocks-one-line-1693.conf