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

This commit is contained in:
sagi
2015-11-22 18:27:23 +00:00
32 changed files with 416 additions and 180 deletions
+1 -2
View File
@@ -60,8 +60,7 @@ addons:
- rsyslog
install: "travis_retry pip install tox coveralls"
before_script: '[ "xxx$BOULDER_INTEGRATION" = "xxx" ] || ./tests/boulder-start.sh'
script: 'travis_retry tox && ([ "xxx$BOULDER_INTEGRATION" = "xxx" ] || (source .tox/$TOXENV/bin/activate && ./tests/boulder-integration.sh))'
script: 'travis_retry tox && ([ "xxx$BOULDER_INTEGRATION" = "xxx" ] || ./tests/travis-integration.sh)'
after_success: '[ "$TOXENV" == "cover" ] && coveralls'
+6 -2
View File
@@ -228,6 +228,9 @@ class HTTP01Response(KeyAuthorizationChallengeResponse):
"""
WHITESPACE_CUTSET = "\n\r\t "
"""Whitespace characters which should be ignored at the end of the body."""
def simple_verify(self, chall, domain, account_public_key, port=None):
"""Simple verify.
@@ -273,10 +276,11 @@ class HTTP01Response(KeyAuthorizationChallengeResponse):
found_ct, chall.CONTENT_TYPE)
return False
if self.key_authorization != http_response.text:
challenge_response = http_response.text.rstrip(self.WHITESPACE_CUTSET)
if self.key_authorization != challenge_response:
logger.debug("Key authorization from response (%r) doesn't match "
"HTTP response (%r)", self.key_authorization,
http_response.text)
challenge_response)
return False
return True
+10
View File
@@ -126,6 +126,16 @@ class HTTP01ResponseTest(unittest.TestCase):
self.assertFalse(self.response.simple_verify(
self.chall, "local", KEY.public_key()))
@mock.patch("acme.challenges.requests.get")
def test_simple_verify_whitespace_validation(self, mock_get):
from acme.challenges import HTTP01Response
mock_get.return_value = mock.MagicMock(
text=(self.chall.validation(KEY) +
HTTP01Response.WHITESPACE_CUTSET), headers=self.good_headers)
self.assertTrue(self.response.simple_verify(
self.chall, "local", KEY.public_key()))
mock_get.assert_called_once_with(self.chall.uri("local"))
@mock.patch("acme.challenges.requests.get")
def test_simple_verify_bad_content_type(self, mock_get):
mock_get().text = self.chall.token
+31 -15
View File
@@ -1,8 +1,8 @@
#!/bin/sh
# Tested with:
# - Fedora 22 (x64)
# - Centos 7 (x64: on AWS EC2 t2.micro, DigitalOcean droplet)
# - Fedora 22, 23 (x64)
# - Centos 7 (x64: onD igitalOcean droplet)
if type yum 2>/dev/null
then
@@ -15,17 +15,33 @@ else
exit 1
fi
# Some distros and older versions of current distros use a "python27"
# instead of "python" naming convention. Try both conventions.
if ! $tool install -y \
python \
python-devel \
python-virtualenv
then
if ! $tool install -y \
python27 \
python27-devel \
python27-virtualenv
then
echo "Could not install Python dependencies. Aborting bootstrap!"
exit 1
fi
fi
# "git-core" seems to be an alias for "git" in CentOS 7 (yum search fails)
# Amazon Linux 2015.03 needs python27-virtualenv rather than python-virtualenv
$tool install -y \
git-core \
python \
python-devel \
python27-virtualenv \
python-virtualenv \
gcc \
dialog \
augeas-libs \
openssl-devel \
libffi-devel \
ca-certificates \
if ! $tool install -y \
git-core \
gcc \
dialog \
augeas-libs \
openssl-devel \
libffi-devel \
ca-certificates
then
echo "Could not install additional dependencies. Aborting bootstrap!"
exit 1
fi
+1 -1
View File
@@ -1,6 +1,6 @@
#!/bin/sh
# SLE12 dont have python-virtualenv
# SLE12 don't have python-virtualenv
zypper -nq in -l git-core \
python \
+1 -1
View File
@@ -105,7 +105,7 @@ https://wiki.mozilla.org/Security/Server_Side_TLS
and the version implemented by the Let's Encrypt client will be the
version that was most current as of the release date of each client
version. Mozilla offers three seperate sets of cryptographic options,
version. Mozilla offers three separate sets of cryptographic options,
which trade off security and compatibility differently. These are
referred to as as the "Modern", "Intermediate", and "Old" configurations
(in order from most secure to least secure, and least-backwards compatible
+88 -24
View File
@@ -42,14 +42,24 @@ To install and run the client you just need to type:
./letsencrypt-auto
.. note:: On RedHat/CentOS 6 you will need to enable the EPEL_
repository before install.
.. _EPEL: http://fedoraproject.org/wiki/EPEL
Throughout the documentation, whenever you see references to
``letsencrypt`` script/binary, you can substitute in
``letsencrypt-auto``. For example, to get the help you would type:
``letsencrypt-auto``. For example, to get basic help you would type:
.. code-block:: shell
./letsencrypt-auto --help
or for full help, type:
.. code-block:: shell
./letsencrypt-auto --help all
Running with Docker
-------------------
@@ -94,6 +104,13 @@ Operating System Packages
* Port: ``cd /usr/ports/security/py-letsencrypt && make install clean``
* Package: ``pkg install py27-letsencrypt``
**Arch Linux**
.. code-block:: shell
sudo pacman -S letsencrypt letsencrypt-nginx letsencrypt-apache \
letshelp-letsencrypt
**Other Operating Systems**
Unfortunately, this is an ongoing effort. If you'd like to package
@@ -128,32 +145,79 @@ SSL certificates!
Plugins
=======
Officially supported plugins:
=========== = = ===============================================================
Plugin A I Notes
=========== = = ===============================================================
apache_ Y Y Automates obtaining and installing a cert with Apache 2.4 on
Debian-based distributions with ``libaugeas0`` 1.0+.
standalone_ Y N Uses a "standalone" webserver to obtain a cert.
webroot_ Y N Obtains a cert using an already running webserver.
manual_ Y N Helps you obtain a cert by giving you instructions to perform
domain validation yourself.
nginx_ Y Y Very experimental and not included in letsencrypt-auto_.
=========== = = ===============================================================
========== = = ================================================================
Plugin A I Notes and status
========== = = ================================================================
standalone Y N Very stable. Uses port 80 (force by
``--standalone-supported-challenges http-01``) or 443
(force by ``--standalone-supported-challenges tls-sni-01``).
apache Y Y Alpha. Automates Apache installation, works fairly well but on
Debian-based distributions only for now.
webroot Y N Works with already running webserver, by writing necessary files
to the disk (``--webroot-path`` should be pointed to your
``public_html``). Currently, when multiple domains are specified
(`-d`), they must all use the same web root path.
manual Y N Hidden from standard UI, use with ``-a manual``. Requires to
copy and paste commands into a new terminal session. Allows to
run client on machine different than target webserver, e.g. your
laptop.
nginx Y Y Very experimental. Not included in letsencrypt-auto_.
========== = = ================================================================
Apache
------
Third party plugins are listed at
https://github.com/letsencrypt/letsencrypt/wiki/Plugins. If
that's not enough, you can always :ref:`write your own plugin
<dev-plugin>`.
If you're running Apache 2.4 on a Debian-based OS with version 1.0+ of
the ``libaugeas0`` package available, you can use the Apache plugin.
This automates both obtaining and installing certs on an Apache
webserver. To specify this plugin on the command line, simply include
``--apache``.
Standalone
----------
To obtain a cert using a "standalone" webserver, you can use the
standalone plugin by including ``certonly`` and ``--standalone``
on the command line. This plugin needs to bind to port 80 or 443 in
order to perform domain validation, so you may need to stop your
existing webserver. To control which port the plugin uses, include
one of the options shown below on the command line.
* ``--standalone-supported-challenges http-01`` to use port 80
* ``--standalone-supported-challenges tls-sni-01`` to use port 443
Webroot
-------
If you're running a webserver that you don't want to stop to use
standalone, you can use the webroot plugin to obtain a cert by
including ``certonly`` and ``-a webroot`` on the command line. In
addition, you'll need to specify ``--webroot-path`` with the root
directory of the files served by your webserver. For example,
``--webroot-path /var/www/html`` or
``--webroot-path /usr/share/nginx/html`` are two common webroot paths.
If multiple domains are specified, they must all use the same path.
Additionally, your server must be configured to serve files from
hidden directories.
Manual
------
If you'd like to obtain a cert running ``letsencrypt`` on a machine
other than your target webserver or perform the steps for domain
validation yourself, you can use the manual plugin. While hidden from
the UI, you can use the plugin to obtain a cert by specifying
``certonly`` and ``-a manual`` on the command line. This requires you
to copy and paste commands into another terminal session.
Nginx
-----
In the future, if you're running Nginx you can use this plugin to
automatically obtain and install your certificate. The Nginx plugin
is still experimental, however, and is not installed with
letsencrypt-auto_. If installed, you can select this plugin on the
command line by including ``--nginx``.
Third party plugins
-------------------
These plugins are listed at
https://github.com/letsencrypt/letsencrypt/wiki/Plugins. If you're
interested, you can also :ref:`write your own plugin <dev-plugin>`.
Renewal
=======
@@ -188,12 +188,8 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
# cert_key... can all be parsed appropriately
self.prepare_server_https("443")
path = {}
path["cert_path"] = self.parser.find_dir(
"SSLCertificateFile", None, vhost.path)
path["cert_key"] = self.parser.find_dir(
"SSLCertificateKeyFile", None, vhost.path)
path = {"cert_path": self.parser.find_dir("SSLCertificateFile", None, vhost.path),
"cert_key": self.parser.find_dir("SSLCertificateKeyFile", None, vhost.path)}
# Only include if a certificate chain is specified
if chain_path is not None:
+3 -1
View File
@@ -85,6 +85,8 @@ ExperimentalBootstrap() {
DeterminePythonVersion() {
if command -v python2.7 > /dev/null ; then
export LE_PYTHON=${LE_PYTHON:-python2.7}
elif command -v python27 > /dev/null ; then
export LE_PYTHON=${LE_PYTHON:-python27}
elif command -v python2 > /dev/null ; then
export LE_PYTHON=${LE_PYTHON:-python2}
elif command -v python > /dev/null ; then
@@ -135,7 +137,7 @@ then
elif uname | grep -iq Darwin ; then
ExperimentalBootstrap "Mac OS X" mac.sh
elif grep -iq "Amazon Linux" /etc/issue ; then
ExperimentalBootstrap "Amazon Linux" amazon_linux.sh
ExperimentalBootstrap "Amazon Linux" _rpm_common.sh
else
echo "Sorry, I don't know how to bootstrap Let's Encrypt on your operating system!"
echo
@@ -34,6 +34,8 @@ def create_le_config(parent_dir):
os.mkdir(config["work_dir"])
os.mkdir(config["logs_dir"])
config["domains"] = None
return argparse.Namespace(**config) # pylint: disable=star-args
@@ -93,7 +93,7 @@ class NginxConfigurator(common.Plugin):
# These will be set in the prepare function
self.parser = None
self.version = version
self._enhance_func = {} # TODO: Support at least redirects
self._enhance_func = {"redirect": self._enable_redirect}
# Set up reverter
self.reverter = reverter.Reverter(self.config)
@@ -344,7 +344,7 @@ class NginxConfigurator(common.Plugin):
##################################
def supported_enhancements(self): # pylint: disable=no-self-use
"""Returns currently supported enhancements."""
return []
return ['redirect']
def enhance(self, domain, enhancement, options=None):
"""Enhance configuration.
@@ -366,6 +366,26 @@ class NginxConfigurator(common.Plugin):
except errors.PluginError:
logger.warn("Failed %s for %s", enhancement, domain)
def _enable_redirect(self, vhost, unused_options):
"""Redirect all equivalent HTTP traffic to ssl_vhost.
Add rewrite directive to non https traffic
.. note:: This function saves the configuration
:param vhost: Destination of traffic, an ssl enabled vhost
:type vhost: :class:`~letsencrypt_nginx.obj.VirtualHost`
:param unused_options: Not currently used
:type unused_options: Not Available
"""
redirect_block = [[['if', '($scheme != "https")'],
[['return', '301 https://$host$request_uri']]
]]
self.parser.add_server_directives(vhost.filep, vhost.names,
redirect_block)
logger.info("Redirecting all traffic to ssl in %s", vhost.filep)
######################################
# Nginx server management (IInstaller)
######################################
@@ -257,6 +257,8 @@ class NginxParser(object):
..note :: If replace is True, this raises a misconfiguration error
if the directive does not already exist.
..note :: If replace is False nothing gets added if an identical
block exists already.
..todo :: Doesn't match server blocks whose server_name directives are
split across multiple conf files.
@@ -411,7 +413,7 @@ def _regex_match(target_name, name):
return True
else:
return False
except re.error:
except re.error: # pragma: no cover
# perl-compatible regexes are sometimes not recognized by python
return False
@@ -448,10 +450,9 @@ def _parse_server(server):
:rtype: dict
"""
parsed_server = {}
parsed_server['addrs'] = set()
parsed_server['ssl'] = False
parsed_server['names'] = set()
parsed_server = {'addrs': set(),
'ssl': False,
'names': set()}
for directive in server:
if directive[0] == 'listen':
@@ -480,7 +481,9 @@ def _add_directives(block, directives, replace=False):
if not replace:
# We insert new directives at the top of the block, mostly
# to work around https://trac.nginx.org/nginx/ticket/810
block.insert(0, directive)
# 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:
@@ -51,11 +51,11 @@ class NginxConfiguratorTest(util.NginxTest):
"example.*", "www.example.org", "myhost"]))
def test_supported_enhancements(self):
self.assertEqual([], self.config.supported_enhancements())
self.assertEqual(['redirect'], self.config.supported_enhancements())
def test_enhance(self):
self.assertRaises(
errors.PluginError, self.config.enhance, 'myhost', 'redirect')
errors.PluginError, self.config.enhance, 'myhost', 'unknown_enhancement')
def test_get_chall_pref(self):
self.assertEqual([challenges.TLSSNI01],
@@ -133,11 +133,11 @@ class NginxParserTest(util.NginxTest):
self.assertEqual(1, len(re.findall(ssl_re, dump)))
server_conf = nparser.abs_path('server.conf')
nparser.add_server_directives(server_conf,
set(['alias', 'another.alias',
'somename']),
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']])
self.assertEqual(nparser.parsed[server_conf],
[['ssl_certificate', '/etc/ssl/cert2.pem'],
['foo', 'bar'],
+26 -8
View File
@@ -69,6 +69,7 @@ USAGE = SHORT_USAGE + """Choice of server plugins for obtaining and installing c
%s
--standalone Run a standalone webserver for authentication
%s
--webroot Place files in a server's webroot folder for authentication
OR use different servers to obtain (authenticate) the cert and then install it:
@@ -80,7 +81,7 @@ More detailed help:
the available topics are:
all, automation, paths, security, testing, or any of the subcommands or
plugins (certonly, install, nginx, apache, standalone, etc)
plugins (certonly, install, nginx, apache, standalone, webroot, etc)
"""
@@ -139,10 +140,8 @@ def _determine_account(args, config):
elif len(accounts) == 1:
acc = accounts[0]
else: # no account registered yet
if args.email is None:
if args.email is None and not args.register_unsafely_without_email:
args.email = display_ops.get_email()
if not args.email: # get_email might return ""
args.email = None
def _tos_cb(regr):
if args.tos:
@@ -380,7 +379,7 @@ def diagnose_configurator_problem(cfg_type, requested, plugins):
raise errors.PluginSelectionError(msg)
def choose_configurator_plugins(args, config, plugins, verb):
def choose_configurator_plugins(args, config, plugins, verb): # pylint: disable=too-many-branches
"""
Figure out which configurator we're going to use
@@ -408,6 +407,10 @@ def choose_configurator_plugins(args, config, plugins, verb):
req_auth = set_configurator(req_auth, "apache")
if args.standalone:
req_auth = set_configurator(req_auth, "standalone")
if args.webroot:
req_auth = set_configurator(req_auth, "webroot")
if args.manual:
req_auth = set_configurator(req_auth, "manual")
logger.debug("Requested authenticator %s and installer %s", req_auth, req_inst)
# Try to meet the user's request and/or ask them to pick plugins
@@ -842,6 +845,16 @@ def prepare_and_parse_args(plugins, args):
helpful.add(
None, "-t", "--text", dest="text_mode", action="store_true",
help="Use the text output instead of the curses UI.")
helpful.add(
None, "--register-unsafely-without-email", action="store_true",
help="Specifying this flag enables registering an account with no "
"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 "
"certificates. Updates to the Subscriber Agreement will still "
"affect you, and will be effective N days after posting an "
"update to the web site.")
helpful.add(None, "-m", "--email", help=config_help("email"))
# positional arg shadows --domains, instead of appending, and
# --domains is useful, because it can be stored in config
@@ -850,7 +863,7 @@ def prepare_and_parse_args(plugins, args):
helpful.add(None, "-d", "--domains", dest="domains",
metavar="DOMAIN", action="append",
help="Domain names to apply. For multiple domains you can use "
"multiple -d flags or enter a comma separated list of domains"
"multiple -d flags or enter a comma separated list of domains "
"as a parameter.")
helpful.add(
None, "--duplicate", dest="duplicate", action="store_true",
@@ -893,8 +906,9 @@ def prepare_and_parse_args(plugins, args):
"testing", "--tls-sni-01-port", type=int,
default=flag_default("tls_sni_01_port"),
help=config_help("tls_sni_01_port"))
helpful.add("testing", "--http-01-port", dest="http01_port", type=int,
help=config_help("http01_port"))
helpful.add(
"testing", "--http-01-port", type=int, dest="http01_port",
default=flag_default("http01_port"), help=config_help("http01_port"))
helpful.add_group(
"security", description="Security parameters & server settings")
@@ -1045,6 +1059,10 @@ def _plugins_parsing(helpful, plugins):
help="Obtain and install certs using Nginx")
helpful.add("plugins", "--standalone", action="store_true",
help='Obtain certs using a "standalone" webserver.')
helpful.add("plugins", "--manual", action="store_true",
help='Provide laborious manual instructions for obtaining a cert')
helpful.add("plugins", "--webroot", action="store_true",
help='Obtain certs by placing files in a webroot directory.')
# things should not be reorder past/pre this comment:
# plugins_group should be displayed in --help before plugin
+31 -2
View File
@@ -100,6 +100,11 @@ def register(config, account_storage, tos_cb=None):
if account_storage.find_all():
logger.info("There are already existing accounts for %s", config.server)
if config.email is None:
if not config.register_unsafely_without_email:
msg = ("No email was provided and "
"--register-unsafely-without-email was not present.")
logger.warn(msg)
raise errors.Error(msg)
logger.warn("Registering without email!")
# Each new registration shall use a fresh new key
@@ -110,7 +115,7 @@ def register(config, account_storage, tos_cb=None):
backend=default_backend())))
acme = acme_from_config_key(config, key)
# TODO: add phone?
regr = acme.register(messages.NewRegistration.from_data(email=config.email))
regr = perform_registration(acme, config)
if regr.terms_of_service is not None:
if tos_cb is not None and not tos_cb(regr):
@@ -126,6 +131,30 @@ def register(config, account_storage, tos_cb=None):
return acc, acme
def perform_registration(acme, config):
"""
Actually register new account, trying repeatedly if there are email
problems
:param .IConfig config: Client configuration.
:param acme.client.Client client: ACME client object.
:returns: Registration Resource.
:rtype: `acme.messages.RegistrationResource`
:raises .UnexpectedUpdate:
"""
try:
return acme.register(messages.NewRegistration.from_data(email=config.email))
except messages.Error, e:
err = repr(e)
if "MX record" in err or "Validation of contact mailto" in err:
config.namespace.email = display_ops.get_email(more=True, invalid=True)
return perform_registration(acme, config)
else:
raise
class Client(object):
"""ACME protocol client.
@@ -455,7 +484,7 @@ class Client(object):
except:
# TODO: suggest letshelp-letsencypt here
reporter.add_message(
"An error occured and we failed to restore your config and "
"An error occurred and we failed to restore your config and "
"restart your server. Please submit a bug report to "
"https://github.com/letsencrypt/letsencrypt",
reporter.HIGH_PRIORITY)
+2 -11
View File
@@ -5,8 +5,6 @@ import re
import zope.interface
from acme import challenges
from letsencrypt import constants
from letsencrypt import errors
from letsencrypt import interfaces
@@ -80,13 +78,6 @@ class NamespaceConfig(object):
return os.path.join(
self.namespace.work_dir, constants.TEMP_CHECKPOINT_DIR)
@property
def http01_port(self): # pylint: disable=missing-docstring
if self.namespace.http01_port is not None:
return self.namespace.http01_port
else:
return challenges.HTTP01Response.PORT
class RenewerConfiguration(object):
"""Configuration wrapper for renewer."""
@@ -155,8 +146,8 @@ def _check_config_domain_sanity(domains):
"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 < 7 chars
# 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,6}$")
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")
+1
View File
@@ -23,6 +23,7 @@ CLI_DEFAULTS = dict(
work_dir="/var/lib/letsencrypt",
logs_dir="/var/log/letsencrypt",
no_verify_ssl=False,
http01_port=challenges.HTTP01Response.PORT,
tls_sni_01_port=challenges.TLSSNI01Response.PORT,
auth_cert_path="./cert.pem",
+31 -10
View File
@@ -34,7 +34,15 @@ def choose_plugin(prepared, question):
question, opts, help_label="More Info")
if code == display_util.OK:
return prepared[index]
plugin_ep = prepared[index]
if plugin_ep.misconfigured:
util(interfaces.IDisplay).notification(
"The selected plugin encountered an error while parsing "
"your server configuration and cannot be used. The error "
"was:\n\n{0}".format(plugin_ep.prepare()),
height=display_util.HEIGHT, pause=False)
else:
return plugin_ep
elif code == display_util.HELP:
if prepared[index].misconfigured:
msg = "Reported Error: %s" % prepared[index].prepare()
@@ -114,23 +122,36 @@ def pick_configurator(
config, default, plugins, question,
(interfaces.IAuthenticator, interfaces.IInstaller))
def get_email():
def get_email(more=False, invalid=False):
"""Prompt for valid email address.
:param bool more: explain why the email is strongly advisable, but how to
skip it
:param bool invalid: true if the user just typed something, but it wasn't
a valid-looking email
:returns: Email or ``None`` if cancelled by user.
:rtype: str
"""
while True:
code, email = zope.component.getUtility(interfaces.IDisplay).input(
"Enter email address (used for urgent notices and lost key recovery)")
msg = "Enter email address (used for urgent notices and lost key recovery)"
if invalid:
msg = "There seem to be problems with that address. " + msg
if more:
msg += ('\n\nIf you really want to skip this, you can run the client with '
'--register-unsafely-without-email but make sure you backup your '
'account key from /etc/letsencrypt/accounts\n\n')
code, email = zope.component.getUtility(interfaces.IDisplay).input(msg)
if code == display_util.OK:
if le_util.safe_email(email):
return email
if code == display_util.OK:
if le_util.safe_email(email):
return email
else:
return None
# TODO catch the server's ACME invalid email address error, and
# make a similar call when that happens
return get_email(more=True, invalid=(email != ""))
else:
return None
def choose_account(accounts):
+7 -1
View File
@@ -104,6 +104,7 @@ class NcursesDisplay(object):
return code, int(tag) - 1
def input(self, message):
"""Display an input box to the user.
@@ -114,7 +115,12 @@ class NcursesDisplay(object):
`string` - input entered by the user
"""
return self.dialog.inputbox(message, width=self.width)
sections = message.split("\n")
# each section takes at least one line, plus extras if it's longer than self.width
wordlines = [1 + (len(section)/self.width) for section in sections]
height = 6 + sum(wordlines) + len(sections)
return self.dialog.inputbox(message, width=self.width, height=height)
def yesno(self, message, yes_label="Yes", no_label="No"):
"""Display a Yes/No dialog box.
+4 -9
View File
@@ -173,17 +173,12 @@ s.serve_forever()" """
uri=achall.chall.uri(achall.domain),
ct=achall.CONTENT_TYPE, command=command))
if response.simple_verify(
if not response.simple_verify(
achall.chall, achall.domain,
achall.account_key.public_key(), self.config.http01_port):
return response
else:
logger.error(
"Self-verify of challenge failed, authorization abandoned.")
if self.conf("test-mode") and self._httpd.poll() is not None:
# simply verify cause command failure...
return False
return None
logger.warning("Self-verify of challenge failed.")
return response
def _notify_and_wait(self, message): # pylint: disable=no-self-use
# TODO: IDisplay wraps messages, breaking the command
+3 -15
View File
@@ -61,7 +61,9 @@ class AuthenticatorTest(unittest.TestCase):
self.assertTrue(self.achalls[0].chall.encode("token") in message)
mock_verify.return_value = False
self.assertEqual([None], self.auth.perform(self.achalls))
with mock.patch("letsencrypt.plugins.manual.logger") as mock_logger:
self.auth.perform(self.achalls)
mock_logger.warning.assert_called_once_with(mock.ANY)
@mock.patch("letsencrypt.plugins.manual.zope.component.getUtility")
@mock.patch("letsencrypt.plugins.manual.Authenticator._notify_and_wait")
@@ -87,20 +89,6 @@ class AuthenticatorTest(unittest.TestCase):
self.assertRaises(
errors.Error, self.auth_test_mode.perform, self.achalls)
@mock.patch("letsencrypt.plugins.manual.socket.socket")
@mock.patch("letsencrypt.plugins.manual.time.sleep", autospec=True)
@mock.patch("acme.challenges.HTTP01Response.simple_verify",
autospec=True)
@mock.patch("letsencrypt.plugins.manual.subprocess.Popen", autospec=True)
def test_perform_test_mode(self, mock_popen, mock_verify, mock_sleep,
mock_socket):
mock_popen.return_value.poll.side_effect = [None, 10]
mock_popen.return_value.pid = 1234
mock_verify.return_value = False
self.assertEqual([False], self.auth_test_mode.perform(self.achalls))
self.assertEqual(1, mock_sleep.call_count)
self.assertEqual(1, mock_socket.call_count)
def test_cleanup_test_mode_already_terminated(self):
# pylint: disable=protected-access
self.auth_test_mode._httpd = httpd = mock.Mock()
+13 -1
View File
@@ -20,6 +20,7 @@ from letsencrypt import errors
from letsencrypt import le_util
from letsencrypt.plugins import disco
from letsencrypt.plugins import manual
from letsencrypt.tests import renewer_test
from letsencrypt.tests import test_util
@@ -201,6 +202,16 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods
self.assertTrue("Could not find configuration root" in ret)
self.assertTrue("NoInstallationError" in ret)
args = ["certonly", "--webroot"]
ret, _, _, _ = self._call(args)
self.assertTrue("--webroot-path must be set" in ret)
with mock.patch("letsencrypt.cli._init_le_client") as mock_init:
with mock.patch("letsencrypt.cli._auth_from_domains"):
self._call(["certonly", "--manual", "-d", "foo.bar"])
auth = mock_init.call_args[0][2]
self.assertTrue(isinstance(auth, manual.Authenticator))
with MockedVerb("certonly") as mock_certonly:
self._call(["auth", "--standalone"])
self.assertEqual(1, mock_certonly.call_count)
@@ -488,7 +499,8 @@ class DetermineAccountTest(unittest.TestCase):
"""Tests for letsencrypt.cli._determine_account."""
def setUp(self):
self.args = mock.MagicMock(account=None, email=None)
self.args = mock.MagicMock(account=None, email=None,
register_unsafely_without_email=False)
self.config = configuration.NamespaceConfig(self.args)
self.accs = [mock.MagicMock(id='x'), mock.MagicMock(id='y')]
self.account_storage = account.AccountMemoryStorage()
+16 -3
View File
@@ -33,7 +33,7 @@ class RegisterTest(unittest.TestCase):
"""Tests for letsencrypt.client.register."""
def setUp(self):
self.config = mock.MagicMock(rsa_key_size=1024)
self.config = mock.MagicMock(rsa_key_size=1024, register_unsafely_without_email=False)
self.account_storage = account.AccountMemoryStorage()
self.tos_cb = mock.MagicMock()
@@ -56,10 +56,23 @@ class RegisterTest(unittest.TestCase):
def test_it(self):
with mock.patch("letsencrypt.client.acme_client.Client"):
with mock.patch("letsencrypt.account."
"report_new_account"):
with mock.patch("letsencrypt.account.report_new_account"):
self._call()
@mock.patch("letsencrypt.account.report_new_account")
@mock.patch("letsencrypt.client.display_ops.get_email")
def test_email_retry(self, _rep, mock_get_email):
from acme import messages
msg = "Validation of contact mailto:sousaphone@improbablylongggstring.tld failed"
mx_err = messages.Error(detail=msg, typ="malformed", title="title")
with mock.patch("letsencrypt.client.acme_client.Client") as mock_client:
mock_client().register.side_effect = [mx_err, mock.MagicMock()]
self._call()
self.assertEqual(mock_get_email.call_count, 1)
def test_needs_email(self):
self.config.email = None
self.assertRaises(errors.Error, self._call)
class ClientTest(unittest.TestCase):
"""Tests for letsencrypt.client.Client."""
-5
View File
@@ -54,11 +54,6 @@ class NamespaceConfigTest(unittest.TestCase):
self.assertEqual(self.config.key_dir, '/tmp/config/keys')
self.assertEqual(self.config.temp_checkpoint_dir, '/tmp/foo/t')
def test_http01_port(self):
self.assertEqual(4321, self.config.http01_port)
self.namespace.http01_port = None
self.assertEqual(80, self.config.http01_port)
def test_absolute_paths(self):
from letsencrypt.configuration import NamespaceConfig
+31 -9
View File
@@ -41,9 +41,11 @@ class ChoosePluginTest(unittest.TestCase):
return choose_plugin(self.plugins, "Question?")
@mock.patch("letsencrypt.display.ops.util")
def test_successful_choice(self, mock_util):
mock_util().menu.return_value = (display_util.OK, 0)
self.assertEqual(self.mock_apache, self._call())
def test_selection(self, mock_util):
mock_util().menu.side_effect = [(display_util.OK, 0),
(display_util.OK, 1)]
self.assertEqual(self.mock_stand, self._call())
self.assertEqual(mock_util().notification.call_count, 1)
@mock.patch("letsencrypt.display.ops.util")
def test_more_info(self, mock_util):
@@ -168,9 +170,9 @@ class GetEmailTest(unittest.TestCase):
zope.component.provideUtility(mock_display, interfaces.IDisplay)
@classmethod
def _call(cls):
def _call(cls, **kwargs):
from letsencrypt.display.ops import get_email
return get_email()
return get_email(**kwargs)
def test_cancel_none(self):
self.input.return_value = (display_util.CANCEL, "foo@bar.baz")
@@ -178,18 +180,38 @@ class GetEmailTest(unittest.TestCase):
def test_ok_safe(self):
self.input.return_value = (display_util.OK, "foo@bar.baz")
with mock.patch("letsencrypt.display.ops.le_util"
".safe_email") as mock_safe_email:
with mock.patch("letsencrypt.display.ops.le_util.safe_email") as mock_safe_email:
mock_safe_email.return_value = True
self.assertTrue(self._call() is "foo@bar.baz")
def test_ok_not_safe(self):
self.input.return_value = (display_util.OK, "foo@bar.baz")
with mock.patch("letsencrypt.display.ops.le_util"
".safe_email") as mock_safe_email:
with mock.patch("letsencrypt.display.ops.le_util.safe_email") as mock_safe_email:
mock_safe_email.side_effect = [False, True]
self.assertTrue(self._call() is "foo@bar.baz")
def test_more_and_invalid_flags(self):
more_txt = "--register-unsafely-without-email"
invalid_txt = "There seem to be problems"
base_txt = "Enter email"
self.input.return_value = (display_util.OK, "foo@bar.baz")
with mock.patch("letsencrypt.display.ops.le_util.safe_email") as mock_safe_email:
mock_safe_email.return_value = True
self._call()
msg = self.input.call_args[0][0]
self.assertTrue(more_txt not in msg)
self.assertTrue(invalid_txt not in msg)
self.assertTrue(base_txt in msg)
self._call(more=True)
msg = self.input.call_args[0][0]
self.assertTrue(more_txt in msg)
self.assertTrue(invalid_txt not in msg)
self._call(more=True, invalid=True)
msg = self.input.call_args[0][0]
self.assertTrue(more_txt in msg)
self.assertTrue(invalid_txt in msg)
self.assertTrue(base_txt in msg)
class ChooseAccountTest(unittest.TestCase):
"""Tests for letsencrypt.display.ops.choose_account."""
+39
View File
@@ -0,0 +1,39 @@
#!/bin/bash
# Download and run Boulder instance for integration testing
# ugh, go version output is like:
# go version go1.4.2 linux/amd64
GOVER=`go version | cut -d" " -f3 | cut -do -f2`
# version comparison
function verlte {
#OS X doesn't support version sorting; emulate with sed
if [ `uname` == 'Darwin' ]; then
[ "$1" = "`echo -e \"$1\n$2\" | sed 's/\b\([0-9]\)\b/0\1/g' \
| sort | sed 's/\b0\([0-9]\)/\1/g' | head -n1`" ]
else
[ "$1" = "`echo -e "$1\n$2" | sort -V | head -n1`" ]
fi
}
if ! verlte 1.5 "$GOVER" ; then
echo "We require go version 1.5 or later; you have... $GOVER"
exit 1
fi
set -xe
# `/...` avoids `no buildable Go source files` errors, for more info
# see `go help packages`
go get -d github.com/letsencrypt/boulder/...
cd $GOPATH/src/github.com/letsencrypt/boulder
# goose is needed for ./test/create_db.sh
wget https://github.com/jsha/boulder-tools/raw/master/goose.gz && \
mkdir $GOPATH/bin && \
zcat goose.gz > $GOPATH/bin/goose && \
chmod +x $GOPATH/bin/goose
./test/create_db.sh
# listenbuddy is needed for ./start.py
go get github.com/jsha/listenbuddy
cd -
+3 -37
View File
@@ -1,43 +1,9 @@
#!/bin/bash
# Download and run Boulder instance for integration testing
# ugh, go version output is like:
# go version go1.4.2 linux/amd64
GOVER=`go version | cut -d" " -f3 | cut -do -f2`
# version comparison
function verlte {
#OS X doesn't support version sorting; emulate with sed
if [ `uname` == 'Darwin' ]; then
[ "$1" = "`echo -e \"$1\n$2\" | sed 's/\b\([0-9]\)\b/0\1/g' \
| sort | sed 's/\b0\([0-9]\)/\1/g' | head -n1`" ]
else
[ "$1" = "`echo -e "$1\n$2" | sort -V | head -n1`" ]
fi
}
if ! verlte 1.5 "$GOVER" ; then
echo "We require go version 1.5 or later; you have... $GOVER"
exit 1
fi
set -xe
export GOPATH="${GOPATH:-/tmp/go}"
export PATH="$GOPATH/bin:$PATH"
# `/...` avoids `no buildable Go source files` errors, for more info
# see `go help packages`
go get -d github.com/letsencrypt/boulder/...
./tests/boulder-fetch.sh
cd $GOPATH/src/github.com/letsencrypt/boulder
# goose is needed for ./test/create_db.sh
wget https://github.com/jsha/boulder-tools/raw/master/goose.gz && \
mkdir $GOPATH/bin && \
zcat goose.gz > $GOPATH/bin/goose && \
chmod +x $GOPATH/bin/goose
./test/create_db.sh
# listenbuddy is needed for ./start.py
go get github.com/jsha/listenbuddy
./start.py &
# Hopefully start.py bootstraps before integration test is started...
./start.py
+1 -1
View File
@@ -23,7 +23,7 @@ letsencrypt_test () {
--no-redirect \
--agree-dev-preview \
--agree-tos \
--email "" \
--register-unsafely-without-email \
--renew-by-default \
--debug \
-vvvvvvv \
+19
View File
@@ -0,0 +1,19 @@
#!/bin/bash
set -o errexit
./tests/boulder-fetch.sh
source .tox/$TOXENV/bin/activate
export LETSENCRYPT_PATH=`pwd`
cd $GOPATH/src/github.com/letsencrypt/boulder/
# boulder's integration-test.py has code that knows to start and wait for the
# boulder processes to start reliably and then will run the letsencrypt
# boulder-interation.sh on its own. The --letsencrypt flag says to run only the
# letsencrypt tests (instead of any other client tests it might run). We're
# going to want to define a more robust interaction point between the boulder
# and letsencrypt tests, but that will be better built off of this.
python test/integration-test.py --letsencrypt
+2 -2
View File
@@ -23,7 +23,7 @@ mv "dist.$version" "dist.$version.$(date +%s).bak" || true
git tag --delete "$tag" || true
tmpvenv=$(mktemp -d)
virtualenv --no-site-packages $tmpvenv
virtualenv --no-site-packages -p python2 $tmpvenv
. $tmpvenv/bin/activate
# update setuptools/pip just like in other places in the repo
pip install -U setuptools
@@ -49,7 +49,7 @@ done
sed -i "s/^__version.*/__version__ = '$version'/" letsencrypt/__init__.py
git add -p # interactive user input
git -c commit.gpgsign=true commit -m "Release $version"
git commit --gpg-sign="$RELEASE_GPG_KEY" -m "Release $version"
git tag --local-user "$RELEASE_GPG_KEY" \
--sign --message "Release $version" "$tag"
+6 -1
View File
@@ -6,7 +6,7 @@
# acme and letsencrypt are not yet on pypi, so when Tox invokes
# "install *.zip", it will not find deps
skipsdist = true
envlist = py26,py27,py33,py34,cover,lint
envlist = py26,py27,py33,py34,py35,cover,lint
# nosetest -v => more verbose output, allows to detect busy waiting
# loops, especially on Travis
@@ -41,6 +41,11 @@ commands =
pip install -e acme[testing]
nosetests -v acme
[testenv:py35]
commands =
pip install -e acme[testing]
nosetests -v acme
[testenv:cover]
basepython = python2.7
commands =