Merge remote-tracking branch 'upstream/master' into bugfix_redirect

This commit is contained in:
sagi 2015-12-11 12:14:02 +00:00
commit 06643b35a0
17 changed files with 203 additions and 64 deletions

View file

@ -27,7 +27,7 @@ If ``letsencrypt`` is packaged for your OS, you can install it from there, and
run it by typing ``letsencrypt``. Because not all operating systems have
packages yet, we provide a temporary solution via the ``letsencrypt-auto``
wrapper script, which obtains some dependencies from your OS and puts others
in an python virtual environment::
in a python virtual environment::
user@webserver:~$ git clone https://github.com/letsencrypt/letsencrypt
user@webserver:~$ cd letsencrypt
@ -128,7 +128,7 @@ launch. The client requires root access in order to write to
bind to ports 80 and 443 (if you use the ``standalone`` plugin) and to read and
modify webserver configurations (if you use the ``apache`` or ``nginx``
plugins). If none of these apply to you, it is theoretically possible to run
without root privilegess, but for most users who want to avoid running an ACME
without root privileges, but for most users who want to avoid running an ACME
client as root, either `letsencrypt-nosudo
<https://github.com/diafygi/letsencrypt-nosudo>`_ or `simp_le
<https://github.com/kuba/simp_le>`_ are more appropriate choices.
@ -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

View file

@ -22,12 +22,14 @@ class Error(jose.JSONObjectWithFields, errors.Error):
('urn:acme:error:' + name, description) for name, description in (
('badCSR', 'The CSR is unacceptable (e.g., due to a short key)'),
('badNonce', 'The client sent an unacceptable anti-replay nonce'),
('connection', 'The server could not connect to the client for DV'),
('connection', 'The server could not connect to the client to '
'verify the domain'),
('dnssec', 'The server could not validate a DNSSEC signed domain'),
('malformed', 'The request message was malformed'),
('rateLimited', 'There were too many requests of a given type'),
('serverInternal', 'The server experienced an internal error'),
('tls', 'The server experienced a TLS error during DV'),
('tls', 'The server experienced a TLS error during domain '
'verification'),
('unauthorized', 'The client lacks sufficient authorization'),
('unknownHost', 'The server could not resolve a domain name'),
)

View file

@ -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

View file

@ -173,10 +173,11 @@ 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`. 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).
`--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
you provide if you do not renew certificates that are about to expire.
@ -286,7 +287,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

View file

@ -11,6 +11,10 @@ server = https://acme-staging.api.letsencrypt.org/directory
# Uncomment and update to register with the specified e-mail address
# email = foo@example.com
# Uncomment and update to generate certificates for the specified
# domains.
# domains = example.com, www.example.com
# Uncomment to use a text interface instead of ncurses
# text = True

View file

@ -1,2 +1,2 @@
Let's Encrypt includes the very latest Augeas lenses in order to ship bug fixes
to Apacche configuration handling bugs as quickly as possible
to Apache configuration handling bugs as quickly as possible

View file

@ -59,8 +59,10 @@ 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 =
@ -77,11 +79,19 @@ let comp = /[<>=]?=/
let arg_dir = [ label "arg" . store (char_arg_dir+|dquot|squot) ]
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 ]
(sep_spc . argv (arg_dir|arg_wordlist))? . eol ]
let section (body:lens) =
(* opt_eol includes empty lines *)
@ -91,7 +101,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)

View file

@ -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.

View file

@ -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])

View file

@ -306,7 +306,7 @@ def _report_new_cert(cert_path, fullchain_path):
def _suggest_donate():
"Suggest a donation to support Let's Encrypt"
reporter_util = zope.component.getUtility(interfaces.IReporter)
msg = ("If like Let's Encrypt, please consider supporting our work by:\n\n"
msg = ("If you like Let's Encrypt, please consider supporting our work by:\n\n"
"Donating to ISRG / Let's Encrypt: https://letsencrypt.org/donate\n"
"Donating to EFF: https://eff.org/donate-le\n\n")
reporter_util.add_message(msg, reporter_util.LOW_PRIORITY)
@ -855,7 +855,7 @@ def prepare_and_parse_args(plugins, args):
"email address. This is strongly discouraged, because in the "
"event of key loss or account compromise you will irrevocably "
"lose access to your account. You will also be unable to receive "
"notice about impending expiration of revocation of your "
"notice about impending expiration or revocation of your "
"certificates. Updates to the Subscriber Agreement will still "
"affect you, and will be effective 14 days after posting an "
"update to the web site.")

View file

@ -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,31 +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")
# 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)

View file

@ -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,

View file

@ -280,3 +280,37 @@ def add_deprecated_argument(add_argument, argument_name, nargs):
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")

View file

@ -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

View file

@ -0,0 +1 @@
SSLRequire %{SSL_CLIENT_S_DN_CN} in {"foo@bar.com", "bar@foo.com"}

View file

@ -0,0 +1,28 @@
<IfModule mod_ssl.c>
<VirtualHost *:443>
ServerAdmin info@somethingnewentertainment.com
ServerName somethingnewentertainment.com
DocumentRoot /var/www/html
ErrorLog /var/log/apache2/error.log
CustomLog /var/log/apache2/access.log combined
SSLEngine on
SSLProtocol all -SSLv2 -SSLv3
SSLHonorCipherOrder on
SSLCipherSuite "EECDH+ECDSA+AESGCM EECDH+aRSA+AESGCM EECDH+ECDSA+SHA384 EEC DH+ECDSA+SHA256 EECDH+aRSA+SHA384 EECDH+aRSA+SHA256 EECDH+aRSA+RC4 EECDH EDH+aRS A RC4 !aNULL !eNULL !LOW !3DES !MD5 !EXP !PSK !SRP !DSS !RC4"
SSLCertificateFile /etc/ssl/certs/ssl-cert-snakeoil.pem
SSLCertificateKeyFile /etc/ssl/private/ssl-cert-snakeoil.key
<FilesMatch "\.(cgi|shtml|phtml|php)$">
SSLOptions +StdEnvVars
</FilesMatch>
<Directory /usr/lib/cgi-bin>
SSLOptions +StdEnvVars
</Directory>
BrowserMatch "MSIE [2-6]" \
nokeepalive ssl-unclean-shutdown \
downgrade-1.0 force-response-1.0
BrowserMatch "MSIE [17-9]" ssl-unclean-shutdown
</VirtualHost> </IfModule>