From 971b34f34207693a114febc44b25da995418645e Mon Sep 17 00:00:00 2001 From: Dev & Sec Date: Fri, 30 Oct 2015 23:16:43 +0000 Subject: [PATCH 01/54] add documentation tell users to enable EPEL --- bootstrap/_rpm_common.sh | 1 - docs/using.rst | 5 +++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/bootstrap/_rpm_common.sh b/bootstrap/_rpm_common.sh index 3fd0f59f9..26b91b8c4 100755 --- a/bootstrap/_rpm_common.sh +++ b/bootstrap/_rpm_common.sh @@ -21,7 +21,6 @@ $tool install -y \ python \ python-devel \ python-virtualenv \ - python-devel \ gcc \ dialog \ augeas-libs \ diff --git a/docs/using.rst b/docs/using.rst index 73c3fe140..6d8182744 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -42,6 +42,11 @@ 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: From d3806a926cc740497b84c8cd2f6f3cdfe0975b6d Mon Sep 17 00:00:00 2001 From: Jeff Hodges Date: Fri, 6 Nov 2015 16:58:40 -0800 Subject: [PATCH 02/54] use boulder's integration-test.py This prevents the integration tests from getting run before the boulder processes have finished booting in most cases. There's still some small races with debug ports going up before RPC ports, but this flushes the big ones (specifically, the WFE ports), and the boulder devs going to fix the rest in integration-test.py over time. This also makes boulder-start.sh a blocking operation. Now the TravisCI integration tests no longer requires boulder-start.sh, we can let the other priority of being easier for users to control (that is, basically, make it easy to Ctrl-C) take over. That plus the idea that self-daemonizing code is tricky to get right, especially over multiple platforms led me to not trying to get start.py to make itself asynchronous. Most of this change is code movement in order to allow developers to run boulder-start.sh once and boulder-integration.sh many times while also not duplicating that code in order to run the tests in TravisCI. I'm not a huge fan of both the letsencrypt's shell scripts and boulder's integration-test.py having hard-coded file dependencies in the other's repo. This, however, seemed like the smallest path to code that would spuriously break less. All the designs I was able to come up that were maybe smaller changes either had the "starts tests before the servers are up" problem or with a "each repo uses another repo's test code file" problem. Those problem on top of the "it's a bigger change" problem led me here. --- .travis.yml | 3 +-- tests/boulder-fetch.sh | 39 ++++++++++++++++++++++++++++++++++++ tests/boulder-start.sh | 40 +++---------------------------------- tests/travis-integration.sh | 19 ++++++++++++++++++ 4 files changed, 62 insertions(+), 39 deletions(-) create mode 100755 tests/boulder-fetch.sh create mode 100755 tests/travis-integration.sh diff --git a/.travis.yml b/.travis.yml index 96e28b1b0..8dde06ceb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -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' diff --git a/tests/boulder-fetch.sh b/tests/boulder-fetch.sh new file mode 100755 index 000000000..a2c31b1d9 --- /dev/null +++ b/tests/boulder-fetch.sh @@ -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 - + diff --git a/tests/boulder-start.sh b/tests/boulder-start.sh index 47c1b6278..acf8f0bbf 100755 --- a/tests/boulder-start.sh +++ b/tests/boulder-start.sh @@ -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 diff --git a/tests/travis-integration.sh b/tests/travis-integration.sh new file mode 100755 index 000000000..3b507bb86 --- /dev/null +++ b/tests/travis-integration.sh @@ -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 From 1014cf5d9e698eacfbc91907b0f11a11ce94620f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20L=C3=A9one?= Date: Wed, 11 Nov 2015 13:23:28 +0100 Subject: [PATCH 03/54] Dict can be litteral --- letsencrypt-apache/letsencrypt_apache/configurator.py | 8 ++------ letsencrypt-nginx/letsencrypt_nginx/parser.py | 7 +++---- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/letsencrypt-apache/letsencrypt_apache/configurator.py b/letsencrypt-apache/letsencrypt_apache/configurator.py index f10f0c241..c811501a9 100644 --- a/letsencrypt-apache/letsencrypt_apache/configurator.py +++ b/letsencrypt-apache/letsencrypt_apache/configurator.py @@ -187,12 +187,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: diff --git a/letsencrypt-nginx/letsencrypt_nginx/parser.py b/letsencrypt-nginx/letsencrypt_nginx/parser.py index fb79703dc..de7ab772c 100644 --- a/letsencrypt-nginx/letsencrypt_nginx/parser.py +++ b/letsencrypt-nginx/letsencrypt_nginx/parser.py @@ -448,10 +448,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': From 324ebf468db402dbe44ba780ed2df682ab99af86 Mon Sep 17 00:00:00 2001 From: Felix Yan Date: Sat, 14 Nov 2015 21:09:03 +0800 Subject: [PATCH 04/54] Update tox.ini to run acme tests with Python 3.5 --- tox.ini | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 9988d8d1f..d1fafe20f 100644 --- a/tox.ini +++ b/tox.ini @@ -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 = From 612344d44fc17eadcbc30ada1c534a19df6f3dc4 Mon Sep 17 00:00:00 2001 From: j Date: Thu, 5 Nov 2015 21:37:10 +0100 Subject: [PATCH 05/54] Ticket #942 - Only insert directive if its not already in block --- letsencrypt-nginx/letsencrypt_nginx/parser.py | 6 +++++- letsencrypt-nginx/letsencrypt_nginx/tests/parser_test.py | 6 +++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/letsencrypt-nginx/letsencrypt_nginx/parser.py b/letsencrypt-nginx/letsencrypt_nginx/parser.py index 19483821a..17167c6a6 100644 --- a/letsencrypt-nginx/letsencrypt_nginx/parser.py +++ b/letsencrypt-nginx/letsencrypt_nginx/parser.py @@ -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. @@ -480,7 +482,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: diff --git a/letsencrypt-nginx/letsencrypt_nginx/tests/parser_test.py b/letsencrypt-nginx/letsencrypt_nginx/tests/parser_test.py index b28640d7f..2d6156429 100644 --- a/letsencrypt-nginx/letsencrypt_nginx/tests/parser_test.py +++ b/letsencrypt-nginx/letsencrypt_nginx/tests/parser_test.py @@ -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'], From b68caac6759e76d566fd568db7fc6ebb0a6e5c83 Mon Sep 17 00:00:00 2001 From: Felix Yan Date: Sun, 15 Nov 2015 21:49:47 +0800 Subject: [PATCH 06/54] Include Arch Linux packages in User Guide --- docs/using.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/using.rst b/docs/using.rst index 8e8fd132e..79248f0f3 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -94,6 +94,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 From ec58ad237800ba14d20f8d045ef58cf3294e4ae5 Mon Sep 17 00:00:00 2001 From: Henry Chen Date: Mon, 16 Nov 2015 16:24:31 +0800 Subject: [PATCH 07/54] Fix Amazon Linux bootstrapping error. --- letsencrypt-auto | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/letsencrypt-auto b/letsencrypt-auto index b3e380f9d..a3009fe52 100755 --- a/letsencrypt-auto +++ b/letsencrypt-auto @@ -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 From 75e7c302f041699c44d1ef6339566e153f6a9378 Mon Sep 17 00:00:00 2001 From: Daniel Aleksandersen Date: Tue, 17 Nov 2015 00:23:58 +0100 Subject: [PATCH 08/54] Support more distros with different RPM names * Support both python and python27 naming conventions (varies by distro) * Test every install command and give feedback if they don't succeed --- bootstrap/_rpm_common.sh | 46 +++++++++++++++++++++++++++------------- 1 file changed, 31 insertions(+), 15 deletions(-) diff --git a/bootstrap/_rpm_common.sh b/bootstrap/_rpm_common.sh index 9f670da6e..5aca13cd4 100755 --- a/bootstrap/_rpm_common.sh +++ b/bootstrap/_rpm_common.sh @@ -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 From 36842b7bbbd52f0086a706264f92b7ba6519ded7 Mon Sep 17 00:00:00 2001 From: Miquel Ruiz Date: Sat, 24 Oct 2015 13:25:04 +0100 Subject: [PATCH 09/54] Ask for email unless --allow-unsafe-registration Add new option that explicitly allows to not provide an email. Fixes #414 --- letsencrypt/cli.py | 14 ++++++++++++-- letsencrypt/display/ops.py | 4 +++- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index e9cb31a21..175354f5d 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -139,9 +139,9 @@ 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.allow_unsafe_registration: args.email = display_ops.get_email() - if not args.email: # get_email might return "" + else: args.email = None def _tos_cb(regr): @@ -842,6 +842,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, "--allow-unsafe-registration", 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 diff --git a/letsencrypt/display/ops.py b/letsencrypt/display/ops.py index 37ce66b62..31913e708 100644 --- a/letsencrypt/display/ops.py +++ b/letsencrypt/display/ops.py @@ -124,7 +124,9 @@ def get_email(): """ while True: code, email = zope.component.getUtility(interfaces.IDisplay).input( - "Enter email address (used for urgent notices and lost key recovery)") + "Enter email address (mandatory since no " + "--allow-unsafe-registration was provided)" + "(used for urgent notices and lost key recovery)") if code == display_util.OK: if le_util.safe_email(email): From 99f9f1b106cc8594feda849d78bdc52f221a9f82 Mon Sep 17 00:00:00 2001 From: Miquel Ruiz Date: Sun, 25 Oct 2015 10:17:25 +0000 Subject: [PATCH 10/54] Rename option and fix displayed info --- letsencrypt/cli.py | 4 ++-- letsencrypt/display/ops.py | 7 ++++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index 175354f5d..a36a2ff4b 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -139,7 +139,7 @@ def _determine_account(args, config): elif len(accounts) == 1: acc = accounts[0] else: # no account registered yet - if args.email is None and not args.allow_unsafe_registration: + if args.email is None and not args.register_unsafely_without_email: args.email = display_ops.get_email() else: args.email = None @@ -843,7 +843,7 @@ def prepare_and_parse_args(plugins, args): None, "-t", "--text", dest="text_mode", action="store_true", help="Use the text output instead of the curses UI.") helpful.add( - None, "--allow-unsafe-registration", action="store_true", + 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 " diff --git a/letsencrypt/display/ops.py b/letsencrypt/display/ops.py index 31913e708..37e18e6d8 100644 --- a/letsencrypt/display/ops.py +++ b/letsencrypt/display/ops.py @@ -124,9 +124,10 @@ def get_email(): """ while True: code, email = zope.component.getUtility(interfaces.IDisplay).input( - "Enter email address (mandatory since no " - "--allow-unsafe-registration was provided)" - "(used for urgent notices and lost key recovery)") + "Enter email address " + "(used for urgent notices and lost key recovery)\n\n" + "If you really want to skip this, run the client with " + "--register-unsafely-without-email") if code == display_util.OK: if le_util.safe_email(email): From 37089b9eff18929ffbc99be0769a2916357baa35 Mon Sep 17 00:00:00 2001 From: Miquel Ruiz Date: Sun, 25 Oct 2015 10:18:06 +0000 Subject: [PATCH 11/54] Ensure cancelling without password exits --- letsencrypt/client.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/letsencrypt/client.py b/letsencrypt/client.py index 8e053e926..959eb9917 100644 --- a/letsencrypt/client.py +++ b/letsencrypt/client.py @@ -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 From 77f2a29bfe8fa71fe3531c05cdb4296f66397631 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Sun, 15 Nov 2015 23:16:05 -0800 Subject: [PATCH 12/54] Show the message about unsafe registration only conditionally - If the user enters a blank email, or one that doesn't check out --- letsencrypt/display/ops.py | 34 ++++++++++++++++++++++------------ 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/letsencrypt/display/ops.py b/letsencrypt/display/ops.py index 37e18e6d8..5a51647a8 100644 --- a/letsencrypt/display/ops.py +++ b/letsencrypt/display/ops.py @@ -114,26 +114,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)\n\n" - "If you really want to skip this, run the client with " - "--register-unsafely-without-email") + 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): From 371e57fc51eb74d385575b7d3701067e617eaccd Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Mon, 16 Nov 2015 11:17:41 -0800 Subject: [PATCH 13/54] If the server rejects an email address, ask again rather than erroring This is essentially symmetrical with cases where the client itself can tell that what the user entered isn't an email address. --- letsencrypt/client.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/letsencrypt/client.py b/letsencrypt/client.py index 959eb9917..236a15a34 100644 --- a/letsencrypt/client.py +++ b/letsencrypt/client.py @@ -115,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): @@ -130,6 +130,21 @@ 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 + + :returns: the same value as acme.register + """ + try: + regr = acme.register(messages.NewRegistration.from_data(email=config.email)) + except messages.Error, e: + if "MX record" in repr(e): + config.namespace.email = display_ops.get_email(more=True, invalid=True) + return perform_registration(acme, config) + else: + raise class Client(object): """ACME protocol client. From 2d07c017b2b922e65e78d1ea6ba8063de5575c7e Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Mon, 16 Nov 2015 12:29:03 -0800 Subject: [PATCH 14/54] Test cases for get_email --- letsencrypt/tests/display/ops_test.py | 32 ++++++++++++++++++++++----- 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/letsencrypt/tests/display/ops_test.py b/letsencrypt/tests/display/ops_test.py index 9d4a3a933..d06d24d4f 100644 --- a/letsencrypt/tests/display/ops_test.py +++ b/letsencrypt/tests/display/ops_test.py @@ -168,9 +168,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 +178,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.""" From c265fb5fb93c294f907a4a074d6aecc25f445463 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Mon, 16 Nov 2015 12:46:26 -0800 Subject: [PATCH 15/54] Fix bugs and test cases --- letsencrypt/cli.py | 2 -- letsencrypt/client.py | 2 +- letsencrypt/tests/cli_test.py | 3 ++- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index a36a2ff4b..f9de4d72f 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -141,8 +141,6 @@ def _determine_account(args, config): else: # no account registered yet if args.email is None and not args.register_unsafely_without_email: args.email = display_ops.get_email() - else: - args.email = None def _tos_cb(regr): if args.tos: diff --git a/letsencrypt/client.py b/letsencrypt/client.py index 236a15a34..0ae0e26dd 100644 --- a/letsencrypt/client.py +++ b/letsencrypt/client.py @@ -138,7 +138,7 @@ def perform_registration(acme, config): :returns: the same value as acme.register """ try: - regr = acme.register(messages.NewRegistration.from_data(email=config.email)) + return acme.register(messages.NewRegistration.from_data(email=config.email)) except messages.Error, e: if "MX record" in repr(e): config.namespace.email = display_ops.get_email(more=True, invalid=True) diff --git a/letsencrypt/tests/cli_test.py b/letsencrypt/tests/cli_test.py index 500ff074e..df4f67928 100644 --- a/letsencrypt/tests/cli_test.py +++ b/letsencrypt/tests/cli_test.py @@ -488,7 +488,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() From 43a6da1309441ca840558deb7c56f3b937f61dfe Mon Sep 17 00:00:00 2001 From: j Date: Thu, 5 Nov 2015 23:20:41 +0100 Subject: [PATCH 16/54] ignore regexp failures in nose coverage --- letsencrypt-nginx/letsencrypt_nginx/parser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/letsencrypt-nginx/letsencrypt_nginx/parser.py b/letsencrypt-nginx/letsencrypt_nginx/parser.py index fb79703dc..873986c76 100644 --- a/letsencrypt-nginx/letsencrypt_nginx/parser.py +++ b/letsencrypt-nginx/letsencrypt_nginx/parser.py @@ -411,7 +411,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 From a790038a9fd615ee5b259828b1570ccfce3d1014 Mon Sep 17 00:00:00 2001 From: j Date: Thu, 5 Nov 2015 19:20:39 +0100 Subject: [PATCH 17/54] nginx: add redirect for HTTP traffic --- .../letsencrypt_nginx/configurator.py | 24 +++++++++++++++++-- .../tests/configurator_test.py | 4 ++-- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/letsencrypt-nginx/letsencrypt_nginx/configurator.py b/letsencrypt-nginx/letsencrypt_nginx/configurator.py index d97cf7397..29445a9d4 100644 --- a/letsencrypt-nginx/letsencrypt_nginx/configurator.py +++ b/letsencrypt-nginx/letsencrypt_nginx/configurator.py @@ -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) ###################################### diff --git a/letsencrypt-nginx/letsencrypt_nginx/tests/configurator_test.py b/letsencrypt-nginx/letsencrypt_nginx/tests/configurator_test.py index 913c5de27..ff720ea85 100644 --- a/letsencrypt-nginx/letsencrypt_nginx/tests/configurator_test.py +++ b/letsencrypt-nginx/letsencrypt_nginx/tests/configurator_test.py @@ -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], From c6bb119d43379f713cafff72b164073b97b66355 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Mon, 16 Nov 2015 17:50:42 -0800 Subject: [PATCH 18/54] Test perform_registration recursion --- letsencrypt/tests/client_test.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/letsencrypt/tests/client_test.py b/letsencrypt/tests/client_test.py index d396e25bc..dec9a0950 100644 --- a/letsencrypt/tests/client_test.py +++ b/letsencrypt/tests/client_test.py @@ -47,10 +47,20 @@ 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 = "No MX record for domain ofijfoisjfs.com" + 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 + self._call() + self.assertEqual(mock_get_email.call_count, 1) + class ClientTest(unittest.TestCase): """Tests for letsencrypt.client.Client.""" From b20acaa3e1ad9957abf7eae2f2557956a23c3e86 Mon Sep 17 00:00:00 2001 From: Matthew Ames Date: Tue, 17 Nov 2015 09:27:15 +0000 Subject: [PATCH 19/54] Update configuration.py --- letsencrypt/configuration.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/letsencrypt/configuration.py b/letsencrypt/configuration.py index 4955655f3..b370c741f 100644 --- a/letsencrypt/configuration.py +++ b/letsencrypt/configuration.py @@ -157,6 +157,6 @@ def _check_config_domain_sanity(domains): # http://www.mkyong.com/regular-expressions/domain-name-regular-expression-example/ # Characters used, domain parts < 63 chars, tld > 1 < 7 chars # first and last char is not "-" - fqdn = re.compile("^((?!-)[A-Za-z0-9-]{1,63}(? Date: Tue, 17 Nov 2015 15:45:52 +0000 Subject: [PATCH 20/54] Update configuration.py --- letsencrypt/configuration.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/letsencrypt/configuration.py b/letsencrypt/configuration.py index b370c741f..7274a1aea 100644 --- a/letsencrypt/configuration.py +++ b/letsencrypt/configuration.py @@ -155,7 +155,7 @@ 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 < 13 chars # first and last char is not "-" fqdn = re.compile("^((?!-)[A-Za-z0-9-]{1,63}(? Date: Tue, 17 Nov 2015 15:02:07 -0800 Subject: [PATCH 21/54] Expanded plugin documentation --- docs/using.rst | 61 +++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 55 insertions(+), 6 deletions(-) diff --git a/docs/using.rst b/docs/using.rst index 334e2e197..a326e4deb 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -140,8 +140,6 @@ SSL certificates! Plugins ======= -Officially supported plugins: - ========== = = ================================================================ Plugin A I Notes and status ========== = = ================================================================ @@ -161,11 +159,62 @@ manual Y N Hidden from standard UI, use with ``-a manual``. Requires to nginx Y Y Very experimental. Not included in letsencrypt-auto_. ========== = = ================================================================ -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 -`. +Apache +------ +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. 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. To use +this plugin, you need to specify ``--webroot-path`` on the command +line 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. + +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 +``-a manual`` or ``--authenticator 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_. + +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 `. Renewal ======= From c82e8691d34f5851cd7ae44eaccb830b9fc77963 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Tue, 17 Nov 2015 15:15:29 -0800 Subject: [PATCH 22/54] Condense table and fixes #1445 --- docs/using.rst | 31 +++++++++++++------------------ 1 file changed, 13 insertions(+), 18 deletions(-) diff --git a/docs/using.rst b/docs/using.rst index a326e4deb..53f0d5d2a 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -140,24 +140,17 @@ SSL certificates! Plugins ======= -========== = = ================================================================ -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_. -========== = = ================================================================ +=========== = = =============================================================== +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_. +=========== = = =============================================================== Apache ------ @@ -190,6 +183,8 @@ line 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 ------ From 78733ae0cb282a1b9e759b2d7dd3eaf6dbd7e8ad Mon Sep 17 00:00:00 2001 From: Matthew Ames Date: Tue, 17 Nov 2015 23:41:26 +0000 Subject: [PATCH 23/54] Changed tld length to be anything over 1 character --- letsencrypt/configuration.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/letsencrypt/configuration.py b/letsencrypt/configuration.py index 7274a1aea..f2d232c22 100644 --- a/letsencrypt/configuration.py +++ b/letsencrypt/configuration.py @@ -155,8 +155,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 < 13 chars + # Characters used, domain parts < 63 chars, tld > 1 char # first and last char is not "-" - fqdn = re.compile("^((?!-)[A-Za-z0-9-]{1,63}(? Date: Mon, 16 Nov 2015 18:31:23 -0800 Subject: [PATCH 24/54] Avoid hacky --email "" case for integration tests --- tests/integration/_common.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/_common.sh b/tests/integration/_common.sh index dbc473728..71d745d93 100755 --- a/tests/integration/_common.sh +++ b/tests/integration/_common.sh @@ -23,7 +23,7 @@ letsencrypt_test () { --no-redirect \ --agree-dev-preview \ --agree-tos \ - --email "" \ + --register-unsafely-without-email \ --renew-by-default \ --debug \ -vvvvvvv \ From 6d497b8076a26830acc139117fcda789b9927df4 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Tue, 17 Nov 2015 12:50:18 -0800 Subject: [PATCH 25/54] Track recent boulder error change --- letsencrypt/client.py | 3 ++- letsencrypt/tests/client_test.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/letsencrypt/client.py b/letsencrypt/client.py index 0ae0e26dd..57ca8d3dd 100644 --- a/letsencrypt/client.py +++ b/letsencrypt/client.py @@ -140,7 +140,8 @@ def perform_registration(acme, config): try: return acme.register(messages.NewRegistration.from_data(email=config.email)) except messages.Error, e: - if "MX record" in repr(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: diff --git a/letsencrypt/tests/client_test.py b/letsencrypt/tests/client_test.py index dec9a0950..45b27f989 100644 --- a/letsencrypt/tests/client_test.py +++ b/letsencrypt/tests/client_test.py @@ -54,7 +54,7 @@ class RegisterTest(unittest.TestCase): @mock.patch("letsencrypt.client.display_ops.get_email") def test_email_retry(self, _rep, mock_get_email): from acme import messages - msg = "No MX record for domain ofijfoisjfs.com" + 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 From 981b9dd3bc13d756f83bf373434a1876e1d705ad Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Tue, 17 Nov 2015 13:43:49 -0800 Subject: [PATCH 26/54] Add test case for trying to register without email --- letsencrypt/tests/client_test.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/letsencrypt/tests/client_test.py b/letsencrypt/tests/client_test.py index 45b27f989..0a7d64a84 100644 --- a/letsencrypt/tests/client_test.py +++ b/letsencrypt/tests/client_test.py @@ -24,7 +24,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() @@ -61,6 +61,9 @@ class RegisterTest(unittest.TestCase): 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.""" From 9a8e6feb54cf8c3c9e723de11797607b8d416c86 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Tue, 17 Nov 2015 16:29:06 -0800 Subject: [PATCH 27/54] Implement --webroot --- letsencrypt/cli.py | 7 ++++++- letsencrypt/tests/cli_test.py | 4 ++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index e9cb31a21..13a5fa3a8 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -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) """ @@ -408,6 +409,8 @@ 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") 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 @@ -1026,6 +1029,8 @@ 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", "--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 diff --git a/letsencrypt/tests/cli_test.py b/letsencrypt/tests/cli_test.py index 500ff074e..6f264b044 100644 --- a/letsencrypt/tests/cli_test.py +++ b/letsencrypt/tests/cli_test.py @@ -201,6 +201,10 @@ 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 MockedVerb("certonly") as mock_certonly: self._call(["auth", "--standalone"]) self.assertEqual(1, mock_certonly.call_count) From ec181965114b981348a735980f07326a04313fe7 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Tue, 17 Nov 2015 16:50:03 -0800 Subject: [PATCH 28/54] lintmonster --- letsencrypt/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index 13a5fa3a8..a8cfbc2cf 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -381,7 +381,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 From 1e09d7e2ff10d5ab9cb7aa8249b16a32f7565eeb Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Tue, 17 Nov 2015 17:11:03 -0800 Subject: [PATCH 29/54] Also implement --manual --- letsencrypt/cli.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index a8cfbc2cf..d641578ed 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -411,6 +411,8 @@ def choose_configurator_plugins(args, config, plugins, verb): # pylint: disable= 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 @@ -1029,6 +1031,8 @@ 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.') From e5fed172296f1566a3c9adec7be611b29c740dcb Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Tue, 17 Nov 2015 17:39:53 -0800 Subject: [PATCH 30/54] Added --manual test --- letsencrypt/tests/cli_test.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/letsencrypt/tests/cli_test.py b/letsencrypt/tests/cli_test.py index 6f264b044..71b580cf0 100644 --- a/letsencrypt/tests/cli_test.py +++ b/letsencrypt/tests/cli_test.py @@ -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 @@ -205,6 +206,12 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods 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) From 369321f90d4c3f0753477b4e98db79e0fbf677d3 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Tue, 17 Nov 2015 17:55:20 -0800 Subject: [PATCH 31/54] Specify flags and certonly --- docs/using.rst | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/docs/using.rst b/docs/using.rst index 53f0d5d2a..5e3554312 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -165,7 +165,8 @@ Standalone ---------- To obtain a cert using a "standalone" webserver, you can use the -standalone plugin. This plugin needs to bind to port 80 or 443 in +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. @@ -177,10 +178,11 @@ 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. To use -this plugin, you need to specify ``--webroot-path`` on the command -line with the root directory of the files served by your webserver. -For example, ``--webroot-path /var/www/html`` or +standalone, you can use the webroot plugin to obtain a cert by +including ``certonly`` and ``--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 @@ -192,9 +194,9 @@ 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 -``-a manual`` or ``--authenticator manual`` on the command line. This -requires you to copy and paste commands into another terminal session. +the UI, you can use the plugin to obtain a cert by specifying +``certonly`` and ``--manual`` on the command line. This requires you +to copy and paste commands into another terminal session. Nginx ----- @@ -202,7 +204,8 @@ 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_. +letsencrypt-auto_. If installed, you can select this plugin on the +command line by including ``--nginx``. Third party plugins ------------------- From 1051c451663edcf06c070dabfdf50923a1fb5e3a Mon Sep 17 00:00:00 2001 From: Matthew Ames Date: Wed, 18 Nov 2015 07:18:20 +0000 Subject: [PATCH 32/54] TLDs cannot be longer than 63 characters --- letsencrypt/configuration.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/letsencrypt/configuration.py b/letsencrypt/configuration.py index f2d232c22..0ea539b5c 100644 --- a/letsencrypt/configuration.py +++ b/letsencrypt/configuration.py @@ -155,8 +155,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 char + # 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}(? Date: Wed, 18 Nov 2015 15:10:00 +0200 Subject: [PATCH 33/54] Update using.rst Advertise the `--help all` command more aggressively --- docs/using.rst | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/using.rst b/docs/using.rst index 334e2e197..2ae301d0f 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -49,12 +49,17 @@ To install and run the client you just need to type: 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 ------------------- From fb844a85a24223d05434f6cd127c636e92016c8b Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 18 Nov 2015 13:16:49 -0800 Subject: [PATCH 34/54] Use -p python2 when creating virtualenv --- tools/dev-release.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/dev-release.sh b/tools/dev-release.sh index 6bbc6ced4..ffaf7881c 100755 --- a/tools/dev-release.sh +++ b/tools/dev-release.sh @@ -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 From d564b8ff8eb3ec403ba54b4311b673ac1e7f4e18 Mon Sep 17 00:00:00 2001 From: Stefan Weil Date: Wed, 18 Nov 2015 22:41:39 +0100 Subject: [PATCH 35/54] Fix typos found by codespell Signed-off-by: Stefan Weil --- bootstrap/_suse_common.sh | 2 +- docs/ciphers.rst | 2 +- letsencrypt/client.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/bootstrap/_suse_common.sh b/bootstrap/_suse_common.sh index 4b41bac36..46f9d693b 100755 --- a/bootstrap/_suse_common.sh +++ b/bootstrap/_suse_common.sh @@ -1,6 +1,6 @@ #!/bin/sh -# SLE12 dont have python-virtualenv +# SLE12 don't have python-virtualenv zypper -nq in -l git-core \ python \ diff --git a/docs/ciphers.rst b/docs/ciphers.rst index 12c403d09..49c0824a3 100644 --- a/docs/ciphers.rst +++ b/docs/ciphers.rst @@ -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 diff --git a/letsencrypt/client.py b/letsencrypt/client.py index 8e053e926..d7113ca25 100644 --- a/letsencrypt/client.py +++ b/letsencrypt/client.py @@ -430,7 +430,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) From 5829e258047d92ef6a464c587c897d7bee62c5a3 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Wed, 18 Nov 2015 14:26:01 -0800 Subject: [PATCH 36/54] Always use the specified GPG for signing everything. --- tools/dev-release.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/dev-release.sh b/tools/dev-release.sh index ffaf7881c..bd86bff44 100755 --- a/tools/dev-release.sh +++ b/tools/dev-release.sh @@ -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" From 02562c75a3688c1cebdc0567bbc381440cc7f204 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Thu, 19 Nov 2015 14:57:31 -0800 Subject: [PATCH 37/54] Remove references to --manual and --webroot --- docs/using.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/using.rst b/docs/using.rst index f6fb82f52..3f04fc5fa 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -184,7 +184,7 @@ 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 ``--webroot`` on the command line. In +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 @@ -200,7 +200,7 @@ 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 ``--manual`` on the command line. This requires you +``certonly`` and ``-a manual`` on the command line. This requires you to copy and paste commands into another terminal session. Nginx From 2e06939fecfc8adca85b1ad28c27dd390e7e33b0 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Thu, 19 Nov 2015 21:15:54 -0800 Subject: [PATCH 38/54] Disable selection of misconfigured plugins --- letsencrypt/display/ops.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/letsencrypt/display/ops.py b/letsencrypt/display/ops.py index 37ce66b62..a240bf847 100644 --- a/letsencrypt/display/ops.py +++ b/letsencrypt/display/ops.py @@ -34,7 +34,14 @@ def choose_plugin(prepared, question): question, opts, help_label="More Info") if code == display_util.OK: - return 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: {0}".format(prepared[index].prepare()) + height=display_util.HEIGHT) + else: + return prepared[index] elif code == display_util.HELP: if prepared[index].misconfigured: msg = "Reported Error: %s" % prepared[index].prepare() From 279c0d9ddf49233d14f976852998cc07d8d562f4 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Thu, 19 Nov 2015 21:16:44 -0800 Subject: [PATCH 39/54] Comma --- letsencrypt/display/ops.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/letsencrypt/display/ops.py b/letsencrypt/display/ops.py index a240bf847..663db9307 100644 --- a/letsencrypt/display/ops.py +++ b/letsencrypt/display/ops.py @@ -38,7 +38,7 @@ def choose_plugin(prepared, question): util(interfaces.IDisplay).notification( "The selected plugin encountered an error while parsing " "your server configuration and cannot be used. The error " - "was: {0}".format(prepared[index].prepare()) + "was: {0}".format(prepared[index].prepare()), height=display_util.HEIGHT) else: return prepared[index] From 2bdc60dfef139dd9bd4e319bb61aea758ead7c0a Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Thu, 19 Nov 2015 21:21:42 -0800 Subject: [PATCH 40/54] Scoping rules are frustrating --- letsencrypt/display/ops.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/letsencrypt/display/ops.py b/letsencrypt/display/ops.py index 663db9307..5724cc542 100644 --- a/letsencrypt/display/ops.py +++ b/letsencrypt/display/ops.py @@ -34,14 +34,15 @@ def choose_plugin(prepared, question): question, opts, help_label="More Info") if code == display_util.OK: + 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: {0}".format(prepared[index].prepare()), + "was: {0}".format(plugin_ep.prepare()), height=display_util.HEIGHT) else: - return prepared[index] + return plugin_ep elif code == display_util.HELP: if prepared[index].misconfigured: msg = "Reported Error: %s" % prepared[index].prepare() From 489e79d77763c07ac8ec0e0f19852f876e896594 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Thu, 19 Nov 2015 22:13:04 -0800 Subject: [PATCH 41/54] spacing --- letsencrypt/display/ops.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/letsencrypt/display/ops.py b/letsencrypt/display/ops.py index 5724cc542..224a701fb 100644 --- a/letsencrypt/display/ops.py +++ b/letsencrypt/display/ops.py @@ -39,7 +39,7 @@ def choose_plugin(prepared, question): util(interfaces.IDisplay).notification( "The selected plugin encountered an error while parsing " "your server configuration and cannot be used. The error " - "was: {0}".format(plugin_ep.prepare()), + "was:\n\n{0}".format(plugin_ep.prepare()), height=display_util.HEIGHT) else: return plugin_ep From 52361cc7305147ec624d3d225ace944c62ae981d Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Thu, 19 Nov 2015 22:18:48 -0800 Subject: [PATCH 42/54] Added tests --- letsencrypt/tests/display/ops_test.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/letsencrypt/tests/display/ops_test.py b/letsencrypt/tests/display/ops_test.py index 9d4a3a933..7427a1dc0 100644 --- a/letsencrypt/tests/display/ops_test.py +++ b/letsencrypt/tests/display/ops_test.py @@ -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): From 350a07086f653158fc9592de7df1b625bffc7ec8 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Thu, 19 Nov 2015 22:35:28 -0800 Subject: [PATCH 43/54] Remove confirmation in text display --- letsencrypt/display/ops.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/letsencrypt/display/ops.py b/letsencrypt/display/ops.py index 224a701fb..dc5904cda 100644 --- a/letsencrypt/display/ops.py +++ b/letsencrypt/display/ops.py @@ -40,7 +40,7 @@ def choose_plugin(prepared, question): "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) + height=display_util.HEIGHT, pause=False) else: return plugin_ep elif code == display_util.HELP: From 52d7baab415de7880b1b26839b0ffde8ad87088c Mon Sep 17 00:00:00 2001 From: Jonathan Herlin Date: Fri, 20 Nov 2015 23:55:20 +0100 Subject: [PATCH 44/54] Added space to avoid printing "domainsas" instead of the correct "domains as" --- letsencrypt/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index d641578ed..e2930c6a1 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -855,7 +855,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", From bb6736f36cfe68fc31595954965635d337df1d24 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Fri, 20 Nov 2015 16:09:39 -0800 Subject: [PATCH 45/54] Remove http01_port magic --- letsencrypt/cli.py | 5 +++-- letsencrypt/configuration.py | 9 --------- letsencrypt/constants.py | 1 + letsencrypt/tests/configuration_test.py | 5 ----- 4 files changed, 4 insertions(+), 16 deletions(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index e2930c6a1..fb71369f1 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -898,8 +898,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") diff --git a/letsencrypt/configuration.py b/letsencrypt/configuration.py index 0ea539b5c..a2a54d2d0 100644 --- a/letsencrypt/configuration.py +++ b/letsencrypt/configuration.py @@ -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.""" diff --git a/letsencrypt/constants.py b/letsencrypt/constants.py index f71bf0329..a402ce923 100644 --- a/letsencrypt/constants.py +++ b/letsencrypt/constants.py @@ -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", diff --git a/letsencrypt/tests/configuration_test.py b/letsencrypt/tests/configuration_test.py index c42b99081..a4f881d34 100644 --- a/letsencrypt/tests/configuration_test.py +++ b/letsencrypt/tests/configuration_test.py @@ -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 From ee5f91f55a7cc43a4952ef6c157f3df7fd5a141e Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Fri, 20 Nov 2015 16:15:50 -0800 Subject: [PATCH 46/54] Set domains to None to satisfy sanity checks --- .../letsencrypt_compatibility_test/util.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/letsencrypt-compatibility-test/letsencrypt_compatibility_test/util.py b/letsencrypt-compatibility-test/letsencrypt_compatibility_test/util.py index 43070cf03..b635ee539 100644 --- a/letsencrypt-compatibility-test/letsencrypt_compatibility_test/util.py +++ b/letsencrypt-compatibility-test/letsencrypt_compatibility_test/util.py @@ -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 From 368f208b7fc9fdaef62e703b1f54680e6b779d7b Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Fri, 20 Nov 2015 18:26:44 -0800 Subject: [PATCH 47/54] Log not fail --- letsencrypt/plugins/manual.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/letsencrypt/plugins/manual.py b/letsencrypt/plugins/manual.py index 07f06ccec..72c5bc259 100644 --- a/letsencrypt/plugins/manual.py +++ b/letsencrypt/plugins/manual.py @@ -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 From 6b23fe160e25a1a5b4a48136fee9b0a03ac60d03 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Fri, 20 Nov 2015 18:34:35 -0800 Subject: [PATCH 48/54] Added tests --- letsencrypt/plugins/manual_test.py | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/letsencrypt/plugins/manual_test.py b/letsencrypt/plugins/manual_test.py index a9281902f..5bde1f909 100644 --- a/letsencrypt/plugins/manual_test.py +++ b/letsencrypt/plugins/manual_test.py @@ -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() From c35c4f3fbebd8cd487d14a7b9f589f76f5129ed8 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Tue, 17 Nov 2015 16:01:53 -0800 Subject: [PATCH 49/54] Extra docstring --- letsencrypt/client.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/letsencrypt/client.py b/letsencrypt/client.py index 57ca8d3dd..486eef198 100644 --- a/letsencrypt/client.py +++ b/letsencrypt/client.py @@ -135,7 +135,13 @@ def perform_registration(acme, config): Actually register new account, trying repeatedly if there are email problems - :returns: the same value as acme.register + :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)) From ec267cf215152091f2c3d7dd4966f9c5da1b704a Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Fri, 20 Nov 2015 14:58:37 -0800 Subject: [PATCH 50/54] "Compute" the minimum height needed to reasonably display input --- letsencrypt/display/util.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/letsencrypt/display/util.py b/letsencrypt/display/util.py index 0e9c76e38..7107bfb3b 100644 --- a/letsencrypt/display/util.py +++ b/letsencrypt/display/util.py @@ -114,7 +114,11 @@ 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. From 1a4d7c144529c4c0986c4620167cde4aa2f3d4eb Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Fri, 20 Nov 2015 15:12:05 -0800 Subject: [PATCH 51/54] Lintmonster --- letsencrypt/client.py | 2 ++ letsencrypt/display/util.py | 6 ++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/letsencrypt/client.py b/letsencrypt/client.py index 486eef198..3dbf9d337 100644 --- a/letsencrypt/client.py +++ b/letsencrypt/client.py @@ -130,6 +130,7 @@ 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 @@ -153,6 +154,7 @@ def perform_registration(acme, config): else: raise + class Client(object): """ACME protocol client. diff --git a/letsencrypt/display/util.py b/letsencrypt/display/util.py index 7107bfb3b..01a8cbc92 100644 --- a/letsencrypt/display/util.py +++ b/letsencrypt/display/util.py @@ -104,6 +104,7 @@ class NcursesDisplay(object): return code, int(tag) - 1 + def input(self, message): """Display an input box to the user. @@ -116,10 +117,11 @@ class NcursesDisplay(object): """ 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) + 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. From 3fec57d8548d4cd200dcae69a5ea8ef2ec3c50f5 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Fri, 20 Nov 2015 17:26:29 -0800 Subject: [PATCH 52/54] Fixed test --- letsencrypt/tests/client_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/letsencrypt/tests/client_test.py b/letsencrypt/tests/client_test.py index 0a7d64a84..160dd55c1 100644 --- a/letsencrypt/tests/client_test.py +++ b/letsencrypt/tests/client_test.py @@ -57,7 +57,7 @@ class RegisterTest(unittest.TestCase): 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_client().register.side_effect = [mx_err, mock.MagicMock()] self._call() self.assertEqual(mock_get_email.call_count, 1) From c3e2c58272fd53fada865d5f0a708dc156c506a4 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Fri, 20 Nov 2015 18:57:57 -0800 Subject: [PATCH 53/54] Fix comment nits --- letsencrypt/display/ops.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/letsencrypt/display/ops.py b/letsencrypt/display/ops.py index 5a51647a8..33a69b2a3 100644 --- a/letsencrypt/display/ops.py +++ b/letsencrypt/display/ops.py @@ -117,9 +117,9 @@ def pick_configurator( 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 + :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 + :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. From 2bc0c31f2ef23bdb13d4ef0e4484670e74897b15 Mon Sep 17 00:00:00 2001 From: Patrick Figel Date: Sat, 21 Nov 2015 01:39:03 +0100 Subject: [PATCH 54/54] Trim trailing whitespace during challenge self-verification fixes #1322 --- acme/acme/challenges.py | 8 ++++++-- acme/acme/challenges_test.py | 10 ++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/acme/acme/challenges.py b/acme/acme/challenges.py index 976d7ab12..336e6c4e5 100644 --- a/acme/acme/challenges.py +++ b/acme/acme/challenges.py @@ -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 diff --git a/acme/acme/challenges_test.py b/acme/acme/challenges_test.py index c4f3d6c61..7cf387ece 100644 --- a/acme/acme/challenges_test.py +++ b/acme/acme/challenges_test.py @@ -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