From 21187b7c1bc7eed2c84e3a5925cfdc32393c1b58 Mon Sep 17 00:00:00 2001 From: Luca Olivetti Date: Fri, 4 Dec 2015 16:10:43 +0100 Subject: [PATCH 001/396] mageia bootstrap --- bootstrap/_mageia_common.sh | 24 ++++++++++++++++++++++++ letsencrypt-auto | 3 +++ 2 files changed, 27 insertions(+) create mode 100755 bootstrap/_mageia_common.sh diff --git a/bootstrap/_mageia_common.sh b/bootstrap/_mageia_common.sh new file mode 100755 index 000000000..9a4606c9d --- /dev/null +++ b/bootstrap/_mageia_common.sh @@ -0,0 +1,24 @@ +#!/bin/sh + +# Tested on mageia 5 x86_64 +if ! urpmi --force \ + python \ + libpython-devel \ + python-virtualenv +then + echo "Could not install Python dependencies. Aborting bootstrap!" + exit 1 +fi + +if ! urpmi --force \ + git \ + gcc \ + cdialog \ + python-augeas \ + libopenssl-devel \ + libffi-devel \ + rootcerts +then + echo "Could not install additional dependencies. Aborting bootstrap!" + exit 1 +fi diff --git a/letsencrypt-auto b/letsencrypt-auto index 44c71883c..13a966a87 100755 --- a/letsencrypt-auto +++ b/letsencrypt-auto @@ -122,6 +122,9 @@ then if [ -f /etc/debian_version ] ; then echo "Bootstrapping dependencies for Debian-based OSes..." $SUDO $BOOTSTRAP/_deb_common.sh + elif [ -f /etc/mageia-release ] ; then + echo "Bootstrapping dependencies for mageia..." + $SUDO $BOOTSTRAP/_mageia_common.sh elif [ -f /etc/redhat-release ] ; then echo "Bootstrapping dependencies for RedHat-based OSes..." $SUDO $BOOTSTRAP/_rpm_common.sh From b7c00a889a607add72823aa20ccf4874a585a8eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torsten=20B=C3=B6gershausen?= Date: Sun, 10 Jan 2016 18:31:49 +0100 Subject: [PATCH 002/396] .gitattributes: EOL=LF overrides auto The line * text=auto eol=lf in .gitattributes does not work as expected: setting eol=lf overrides text=auto, and works as * eol=lf or * text eol=lf This is not what is intended, as binary files like *.gz go through a CRLF -> LF conversion at commit time. Use * crlf=auto instead (which is even understood by older Git versions, which are still in use) --- .gitattributes | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.gitattributes b/.gitattributes index 5eee84cce..2a18bd183 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,7 +1,15 @@ -* text=auto eol=lf +#Default, normalize CRLF into LF in non-binary files +# Files identified as binary by Git are not changed +* crlf=auto # special files +*.sh crlf=input +*.py crlf=input + *.bat text eol=crlf + +*.der binary +*.gz binary *.jpeg binary *.jpg binary *.png binary From 675e6e212a23d37cfd00e507c8e43c158721e2c4 Mon Sep 17 00:00:00 2001 From: Jacob Hoffman-Andrews Date: Tue, 15 Mar 2016 00:58:47 -0700 Subject: [PATCH 003/396] Add --must-staple flag. --- letsencrypt/cli.py | 3 +++ letsencrypt/crypto_util.py | 20 ++++++++++++++------ letsencrypt/interfaces.py | 2 ++ letsencrypt/tests/crypto_util_test.py | 19 +++++++++++++++++++ 4 files changed, 38 insertions(+), 6 deletions(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index 8e545a5de..cbd3b0704 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -1589,6 +1589,9 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): helpful.add( "security", "--rsa-key-size", type=int, metavar="N", default=flag_default("rsa_key_size"), help=config_help("rsa_key_size")) + helpful.add( + "security", "--must-staple", action="store_true", + help=config_help("must_staple"), dest="must_staple", default=False) helpful.add( "security", "--redirect", action="store_true", help="Automatically redirect all HTTP traffic to HTTPS for the newly " diff --git a/letsencrypt/crypto_util.py b/letsencrypt/crypto_util.py index 5fdcba843..7856c13cc 100644 --- a/letsencrypt/crypto_util.py +++ b/letsencrypt/crypto_util.py @@ -75,9 +75,11 @@ def init_save_csr(privkey, names, path, csrname="csr-letsencrypt.pem"): :rtype: :class:`letsencrypt.le_util.CSR` """ - csr_pem, csr_der = make_csr(privkey.pem, names) - config = zope.component.getUtility(interfaces.IConfig) + + csr_pem, csr_der = make_csr(privkey.pem, names, + must_staple=config.must_staple) + # Save CSR le_util.make_or_verify_dir(path, 0o755, os.geteuid(), config.strict_permissions) @@ -92,7 +94,7 @@ def init_save_csr(privkey, names, path, csrname="csr-letsencrypt.pem"): # Lower level functions -def make_csr(key_str, domains): +def make_csr(key_str, domains, must_staple=False): """Generate a CSR. :param str key_str: PEM-encoded RSA key. @@ -111,13 +113,19 @@ def make_csr(key_str, domains): req.get_subject().CN = domains[0] # TODO: what to put into req.get_subject()? # TODO: put SAN if len(domains) > 1 - req.add_extensions([ + extensions = [ OpenSSL.crypto.X509Extension( "subjectAltName", critical=False, value=", ".join("DNS:%s" % d for d in domains) - ), - ]) + ) + ] + if must_staple: + extensions.append(OpenSSL.crypto.X509Extension( + "1.3.6.1.5.5.7.1.24", + critical=False, + value="DER:30:03:02:01:05")) + req.add_extensions(extensions) req.set_version(2) req.set_pubkey(pkey) req.sign(pkey, "sha256") diff --git a/letsencrypt/interfaces.py b/letsencrypt/interfaces.py index 1921b1e54..be82787b5 100644 --- a/letsencrypt/interfaces.py +++ b/letsencrypt/interfaces.py @@ -200,6 +200,8 @@ class IConfig(zope.interface.Interface): email = zope.interface.Attribute( "Email used for registration and recovery contact.") rsa_key_size = zope.interface.Attribute("Size of the RSA key.") + must_staple = zope.interface.Attribute( + "Whether to request the OCSP Must Staple extension.") config_dir = zope.interface.Attribute("Configuration directory.") work_dir = zope.interface.Attribute("Working directory.") diff --git a/letsencrypt/tests/crypto_util_test.py b/letsencrypt/tests/crypto_util_test.py index 1a9f39572..ec35fc688 100644 --- a/letsencrypt/tests/crypto_util_test.py +++ b/letsencrypt/tests/crypto_util_test.py @@ -95,6 +95,25 @@ class MakeCSRTest(unittest.TestCase): ['example.com', 'www.example.com'], get_sans_from_csr( csr_der, OpenSSL.crypto.FILETYPE_ASN1)) + def test_must_staple(self): + # TODO: Fails for RSA256_KEY + csr_pem, _ = self._call( + RSA512_KEY, ['example.com', 'www.example.com'], must_staple=True) + csr = OpenSSL.crypto.load_certificate_request( + OpenSSL.crypto.FILETYPE_PEM, csr_pem) + + # In pyopenssl 0.13 (used with TOXENV=py26-oldest and py27-oldest), csr + # objects don't have a get_extensions() method, so we skip this test if + # the method isn't available. + if hasattr(csr, 'get_extensions'): + # NOTE: Ideally we would filter by the TLS Feature OID, but + # OpenSSL.crypto.X509Extension doesn't give us the extension's raw OID, + # and the shortname field is just "UNDEF" + must_staple_exts = [e for e in csr.get_extensions() + if e.get_data() == "0\x03\x02\x01\x05"] + self.assertEqual(len(must_staple_exts), 1, + "Expected exactly one Must Staple extension") + class ValidCSRTest(unittest.TestCase): """Tests for letsencrypt.crypto_util.valid_csr.""" From 82beb6f705093f5e82b2c9bdb8013276dcb92939 Mon Sep 17 00:00:00 2001 From: Jacob Hoffman-Andrews Date: Wed, 16 Mar 2016 16:25:41 -0700 Subject: [PATCH 004/396] Update must-staple help. --- letsencrypt/interfaces.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/letsencrypt/interfaces.py b/letsencrypt/interfaces.py index be82787b5..a2e8506e6 100644 --- a/letsencrypt/interfaces.py +++ b/letsencrypt/interfaces.py @@ -201,7 +201,9 @@ class IConfig(zope.interface.Interface): "Email used for registration and recovery contact.") rsa_key_size = zope.interface.Attribute("Size of the RSA key.") must_staple = zope.interface.Attribute( - "Whether to request the OCSP Must Staple extension.") + "Whether to request the OCSP Must Staple certificate extension. " + "Additional setup may be required after issuance. This does not " + "currently autoconfigure web servers for OCSP stapling. ") config_dir = zope.interface.Attribute("Configuration directory.") work_dir = zope.interface.Attribute("Working directory.") From 15502bb64e50b75a4a5e46a29179492c1b5c9fda Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Thu, 17 Mar 2016 16:14:29 -0700 Subject: [PATCH 005/396] renew implies noninteractive should be a property of config Not a property of the config we change later --- letsencrypt/cli.py | 3 +++ letsencrypt/main.py | 3 --- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index 017e0a62e..d3ffd3441 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -409,6 +409,9 @@ class HelpfulArgumentParser(object): # Do any post-parsing homework here + if self.verb == "renew": + self.noninteractive_mode = True + # we get domains from -d, but also from the webroot map... if parsed_args.webroot_map: for domain in parsed_args.webroot_map.keys(): diff --git a/letsencrypt/main.py b/letsencrypt/main.py index 8d59993df..0148bdeca 100644 --- a/letsencrypt/main.py +++ b/letsencrypt/main.py @@ -685,9 +685,6 @@ def main(cli_args=sys.argv[1:]): displayer = display_util.NoninteractiveDisplay(sys.stdout) elif config.text_mode: displayer = display_util.FileDisplay(sys.stdout) - elif config.verb == "renew": - config.noninteractive_mode = True - displayer = display_util.NoninteractiveDisplay(sys.stdout) else: displayer = display_util.NcursesDisplay() zope.component.provideUtility(displayer) From bd7d85fd601eab333d83bd7264bc5bc2bef4c3e9 Mon Sep 17 00:00:00 2001 From: Maikel Date: Mon, 21 Mar 2016 13:52:56 +0100 Subject: [PATCH 006/396] Fix symlink webroot paths When using symlinks for webroot folders and requesting SSL for same websites with symlink domains. The challenges files and acme-challenge directory are removed on the first domain and on second domain the acme-challenge directory is already removed and tries to remove it again. --- letsencrypt/plugins/webroot.py | 3 +++ letsencrypt/plugins/webroot_test.py | 15 ++++++++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/letsencrypt/plugins/webroot.py b/letsencrypt/plugins/webroot.py index 6d2899511..6254719ed 100644 --- a/letsencrypt/plugins/webroot.py +++ b/letsencrypt/plugins/webroot.py @@ -155,5 +155,8 @@ to serve all files under specified web root ({0}).""" elif exc.errno == errno.EACCES: logger.debug("Challenges cleaned up but no permissions for %s", root_path) + elif exc.errno == errno.ENOENT: + logger.debug("Challenges cleaned up, %s does not exists", + root_path) else: raise diff --git a/letsencrypt/plugins/webroot_test.py b/letsencrypt/plugins/webroot_test.py index ed0326555..b39e3912a 100644 --- a/letsencrypt/plugins/webroot_test.py +++ b/letsencrypt/plugins/webroot_test.py @@ -176,12 +176,25 @@ class AuthenticatorTest(unittest.TestCase): self.auth.perform([self.achall]) os_error = OSError() - os_error.errno = errno.ENOENT + os_error.errno = errno.EPERM mock_rmdir.side_effect = os_error self.assertRaises(OSError, self.auth.cleanup, [self.achall]) self.assertFalse(os.path.exists(self.validation_path)) self.assertTrue(os.path.exists(self.root_challenge_path)) + @mock.patch('os.rmdir') + def test_cleanup_file_not_exists(self, mock_rmdir): + self.auth.prepare() + self.auth.perform([self.achall]) + + os_error = OSError() + os_error.errno = errno.ENOENT + mock_rmdir.side_effect = os_error + + self.auth.cleanup([self.achall]) + self.assertFalse(os.path.exists(self.validation_path)) + self.assertTrue(os.path.exists(self.root_challenge_path)) + if __name__ == "__main__": unittest.main() # pragma: no cover From e2af5ab9b495ca22009b5b251cd2d1df48635872 Mon Sep 17 00:00:00 2001 From: Noah Swartz Date: Fri, 1 Apr 2016 16:42:44 -0700 Subject: [PATCH 007/396] updated docs with s/letsencrypt/certbot/g and more --- docs/api/account.rst | 4 +- docs/api/achallenges.rst | 4 +- docs/api/auth_handler.rst | 4 +- docs/api/cb_util.rst | 5 ++ docs/api/client.rst | 4 +- docs/api/configuration.rst | 4 +- docs/api/constants.rst | 4 +- docs/api/continuity_auth.rst | 4 +- docs/api/crypto_util.rst | 4 +- docs/api/display.rst | 16 ++-- docs/api/errors.rst | 4 +- docs/api/index.rst | 4 +- docs/api/interfaces.rst | 4 +- docs/api/le_util.rst | 5 -- docs/api/log.rst | 4 +- docs/api/plugins/common.rst | 4 +- docs/api/plugins/disco.rst | 4 +- docs/api/plugins/manual.rst | 4 +- docs/api/plugins/standalone.rst | 4 +- docs/api/plugins/util.rst | 4 +- docs/api/plugins/webroot.rst | 4 +- docs/api/proof_of_possession.rst | 4 +- docs/api/reporter.rst | 4 +- docs/api/reverter.rst | 4 +- docs/api/storage.rst | 4 +- docs/ciphers.rst | 80 ++++++++--------- docs/contributing.rst | 60 ++++++------- docs/index.rst | 2 +- docs/man/certbot.rst | 1 + docs/man/letsencrypt.rst | 1 - docs/packaging.rst | 2 +- docs/using.rst | 146 +++++++++++++++---------------- 32 files changed, 201 insertions(+), 205 deletions(-) create mode 100644 docs/api/cb_util.rst delete mode 100644 docs/api/le_util.rst create mode 100644 docs/man/certbot.rst delete mode 100644 docs/man/letsencrypt.rst diff --git a/docs/api/account.rst b/docs/api/account.rst index 16c2061a8..fd90230ea 100644 --- a/docs/api/account.rst +++ b/docs/api/account.rst @@ -1,5 +1,5 @@ -:mod:`letsencrypt.account` +:mod:`certbot.account` -------------------------- -.. automodule:: letsencrypt.account +.. automodule:: certbot.account :members: diff --git a/docs/api/achallenges.rst b/docs/api/achallenges.rst index 09cec1702..90dda3f06 100644 --- a/docs/api/achallenges.rst +++ b/docs/api/achallenges.rst @@ -1,5 +1,5 @@ -:mod:`letsencrypt.achallenges` +:mod:`certbot.achallenges` ------------------------------ -.. automodule:: letsencrypt.achallenges +.. automodule:: certbot.achallenges :members: diff --git a/docs/api/auth_handler.rst b/docs/api/auth_handler.rst index 3b168faf8..8819bb1bd 100644 --- a/docs/api/auth_handler.rst +++ b/docs/api/auth_handler.rst @@ -1,5 +1,5 @@ -:mod:`letsencrypt.auth_handler` +:mod:`certbot.auth_handler` ------------------------------- -.. automodule:: letsencrypt.auth_handler +.. automodule:: certbot.auth_handler :members: diff --git a/docs/api/cb_util.rst b/docs/api/cb_util.rst new file mode 100644 index 000000000..066fa906c --- /dev/null +++ b/docs/api/cb_util.rst @@ -0,0 +1,5 @@ +:mod:`certbot.cb_util` +-------------------------- + +.. automodule:: certbot.cb_util + :members: diff --git a/docs/api/client.rst b/docs/api/client.rst index 7fe44df50..00a443cd9 100644 --- a/docs/api/client.rst +++ b/docs/api/client.rst @@ -1,5 +1,5 @@ -:mod:`letsencrypt.client` +:mod:`certbot.client` ------------------------- -.. automodule:: letsencrypt.client +.. automodule:: certbot.client :members: diff --git a/docs/api/configuration.rst b/docs/api/configuration.rst index e92392b99..4e99c73d2 100644 --- a/docs/api/configuration.rst +++ b/docs/api/configuration.rst @@ -1,5 +1,5 @@ -:mod:`letsencrypt.configuration` +:mod:`certbot.configuration` -------------------------------- -.. automodule:: letsencrypt.configuration +.. automodule:: certbot.configuration :members: diff --git a/docs/api/constants.rst b/docs/api/constants.rst index 3a2815b5e..e225056a2 100644 --- a/docs/api/constants.rst +++ b/docs/api/constants.rst @@ -1,5 +1,5 @@ -:mod:`letsencrypt.constants` +:mod:`certbot.constants` ----------------------------------- -.. automodule:: letsencrypt.constants +.. automodule:: certbot.constants :members: diff --git a/docs/api/continuity_auth.rst b/docs/api/continuity_auth.rst index 82869e6f4..3276220f5 100644 --- a/docs/api/continuity_auth.rst +++ b/docs/api/continuity_auth.rst @@ -1,5 +1,5 @@ -:mod:`letsencrypt.continuity_auth` +:mod:`certbot.continuity_auth` ---------------------------------- -.. automodule:: letsencrypt.continuity_auth +.. automodule:: certbot.continuity_auth :members: diff --git a/docs/api/crypto_util.rst b/docs/api/crypto_util.rst index 5d4c77538..2f473944c 100644 --- a/docs/api/crypto_util.rst +++ b/docs/api/crypto_util.rst @@ -1,5 +1,5 @@ -:mod:`letsencrypt.crypto_util` +:mod:`certbot.crypto_util` ------------------------------ -.. automodule:: letsencrypt.crypto_util +.. automodule:: certbot.crypto_util :members: diff --git a/docs/api/display.rst b/docs/api/display.rst index 117a91708..1a18e6534 100644 --- a/docs/api/display.rst +++ b/docs/api/display.rst @@ -1,23 +1,23 @@ -:mod:`letsencrypt.display` +:mod:`certbot.display` -------------------------- -.. automodule:: letsencrypt.display +.. automodule:: certbot.display :members: -:mod:`letsencrypt.display.util` +:mod:`certbot.display.util` =============================== -.. automodule:: letsencrypt.display.util +.. automodule:: certbot.display.util :members: -:mod:`letsencrypt.display.ops` +:mod:`certbot.display.ops` ============================== -.. automodule:: letsencrypt.display.ops +.. automodule:: certbot.display.ops :members: -:mod:`letsencrypt.display.enhancements` +:mod:`certbot.display.enhancements` ======================================= -.. automodule:: letsencrypt.display.enhancements +.. automodule:: certbot.display.enhancements :members: diff --git a/docs/api/errors.rst b/docs/api/errors.rst index 1ad13235c..a9324765b 100644 --- a/docs/api/errors.rst +++ b/docs/api/errors.rst @@ -1,5 +1,5 @@ -:mod:`letsencrypt.errors` +:mod:`certbot.errors` ------------------------- -.. automodule:: letsencrypt.errors +.. automodule:: certbot.errors :members: diff --git a/docs/api/index.rst b/docs/api/index.rst index a2475eeae..be94214c9 100644 --- a/docs/api/index.rst +++ b/docs/api/index.rst @@ -1,5 +1,5 @@ -:mod:`letsencrypt` +:mod:`certbot` ------------------ -.. automodule:: letsencrypt +.. automodule:: certbot :members: diff --git a/docs/api/interfaces.rst b/docs/api/interfaces.rst index 00b0a1e50..2988b3b87 100644 --- a/docs/api/interfaces.rst +++ b/docs/api/interfaces.rst @@ -1,5 +1,5 @@ -:mod:`letsencrypt.interfaces` +:mod:`certbot.interfaces` ----------------------------- -.. automodule:: letsencrypt.interfaces +.. automodule:: certbot.interfaces :members: diff --git a/docs/api/le_util.rst b/docs/api/le_util.rst deleted file mode 100644 index 8c6b717cf..000000000 --- a/docs/api/le_util.rst +++ /dev/null @@ -1,5 +0,0 @@ -:mod:`letsencrypt.le_util` --------------------------- - -.. automodule:: letsencrypt.le_util - :members: diff --git a/docs/api/log.rst b/docs/api/log.rst index f41c6c4b1..41311de90 100644 --- a/docs/api/log.rst +++ b/docs/api/log.rst @@ -1,5 +1,5 @@ -:mod:`letsencrypt.log` +:mod:`certbot.log` ---------------------- -.. automodule:: letsencrypt.log +.. automodule:: certbot.log :members: diff --git a/docs/api/plugins/common.rst b/docs/api/plugins/common.rst index ca55ba8fb..7cfaf8d70 100644 --- a/docs/api/plugins/common.rst +++ b/docs/api/plugins/common.rst @@ -1,5 +1,5 @@ -:mod:`letsencrypt.plugins.common` +:mod:`certbot.plugins.common` --------------------------------- -.. automodule:: letsencrypt.plugins.common +.. automodule:: certbot.plugins.common :members: diff --git a/docs/api/plugins/disco.rst b/docs/api/plugins/disco.rst index 7bf2b76b4..1a27f0f69 100644 --- a/docs/api/plugins/disco.rst +++ b/docs/api/plugins/disco.rst @@ -1,5 +1,5 @@ -:mod:`letsencrypt.plugins.disco` +:mod:`certbot.plugins.disco` -------------------------------- -.. automodule:: letsencrypt.plugins.disco +.. automodule:: certbot.plugins.disco :members: diff --git a/docs/api/plugins/manual.rst b/docs/api/plugins/manual.rst index 4661ab7df..eea443499 100644 --- a/docs/api/plugins/manual.rst +++ b/docs/api/plugins/manual.rst @@ -1,5 +1,5 @@ -:mod:`letsencrypt.plugins.manual` +:mod:`certbot.plugins.manual` --------------------------------- -.. automodule:: letsencrypt.plugins.manual +.. automodule:: certbot.plugins.manual :members: diff --git a/docs/api/plugins/standalone.rst b/docs/api/plugins/standalone.rst index f5b9d9c24..60aa48b4f 100644 --- a/docs/api/plugins/standalone.rst +++ b/docs/api/plugins/standalone.rst @@ -1,5 +1,5 @@ -:mod:`letsencrypt.plugins.standalone` +:mod:`certbot.plugins.standalone` ------------------------------------- -.. automodule:: letsencrypt.plugins.standalone +.. automodule:: certbot.plugins.standalone :members: diff --git a/docs/api/plugins/util.rst b/docs/api/plugins/util.rst index 6bc8995db..30ab3d49f 100644 --- a/docs/api/plugins/util.rst +++ b/docs/api/plugins/util.rst @@ -1,5 +1,5 @@ -:mod:`letsencrypt.plugins.util` +:mod:`certbot.plugins.util` ------------------------------- -.. automodule:: letsencrypt.plugins.util +.. automodule:: certbot.plugins.util :members: diff --git a/docs/api/plugins/webroot.rst b/docs/api/plugins/webroot.rst index 339d546a5..e1f4523f7 100644 --- a/docs/api/plugins/webroot.rst +++ b/docs/api/plugins/webroot.rst @@ -1,5 +1,5 @@ -:mod:`letsencrypt.plugins.webroot` +:mod:`certbot.plugins.webroot` ---------------------------------- -.. automodule:: letsencrypt.plugins.webroot +.. automodule:: certbot.plugins.webroot :members: diff --git a/docs/api/proof_of_possession.rst b/docs/api/proof_of_possession.rst index db8c6c563..2e7642a45 100644 --- a/docs/api/proof_of_possession.rst +++ b/docs/api/proof_of_possession.rst @@ -1,5 +1,5 @@ -:mod:`letsencrypt.proof_of_possession` +:mod:`certbot.proof_of_possession` -------------------------------------- -.. automodule:: letsencrypt.proof_of_possession +.. automodule:: certbot.proof_of_possession :members: diff --git a/docs/api/reporter.rst b/docs/api/reporter.rst index 03260f9cd..ad71dbb69 100644 --- a/docs/api/reporter.rst +++ b/docs/api/reporter.rst @@ -1,5 +1,5 @@ -:mod:`letsencrypt.reporter` +:mod:`certbot.reporter` --------------------------- -.. automodule:: letsencrypt.reporter +.. automodule:: certbot.reporter :members: diff --git a/docs/api/reverter.rst b/docs/api/reverter.rst index 4c220124f..3e0ac750b 100644 --- a/docs/api/reverter.rst +++ b/docs/api/reverter.rst @@ -1,5 +1,5 @@ -:mod:`letsencrypt.reverter` +:mod:`certbot.reverter` --------------------------- -.. automodule:: letsencrypt.reverter +.. automodule:: certbot.reverter :members: diff --git a/docs/api/storage.rst b/docs/api/storage.rst index 198d85b46..34e3a45c0 100644 --- a/docs/api/storage.rst +++ b/docs/api/storage.rst @@ -1,5 +1,5 @@ -:mod:`letsencrypt.storage` +:mod:`certbot.storage` -------------------------- -.. automodule:: letsencrypt.storage +.. automodule:: certbot.storage :members: diff --git a/docs/ciphers.rst b/docs/ciphers.rst index ef644b7a0..be6784276 100644 --- a/docs/ciphers.rst +++ b/docs/ciphers.rst @@ -17,15 +17,13 @@ Autoupdates Within certain limits, TLS server software can choose what kind of cryptography to use when a client connects. These choices can affect security, compatibility, and performance in complex ways. Most of -these options are independent of a particular certificate. The Let's -Encrypt client tries to provide defaults that we think are most useful -to our users. +these options are independent of a particular certificate. Certbot +tries to provide defaults that we think are most useful to our users. -As described below, the Let's Encrypt client will default to modifying +As described below, Certbot will default to modifying server software's cryptographic settings to keep these up-to-date with -what we think are appropriate defaults when new versions of the Let's -Encrypt client are installed (for example, by an operating system package -manager). +what we think are appropriate defaults when new versions of the Certbot +are installed (for example, by an operating system package manager). When this feature is implemented, this document will be updated to describe how to disable these automatic changes. @@ -54,7 +52,7 @@ improve, others' security. But important information that improves our understanding of the state of the art is published regularly. When enabling TLS support in a compatible web server (which is a separate -step from obtaining a certificate), Let's Encrypt has the ability to +step from obtaining a certificate), Certbot has the ability to update that web server's TLS configuration. Again, this is *different from the cryptographic particulars of the certificate itself*; the certificate as of the initial release will be RSA-signed using one of @@ -80,30 +78,29 @@ art. However, the Let's Encrypt certificate authority does *not* dictate end-users' security policy, and any site is welcome to change its preferences in accordance with its own policy or its administrators' preferences, and use different cryptographic mechanisms or parameters, -or a different priority order, than the defaults provided by the Let's -Encrypt client. +or a different priority order, than the defaults provided by Certbot. -If you don't use the Let's Encrypt client to configure your server -directly, because the client doesn't integrate with your server software -or because you chose not to use this integration, then the cryptographic -defaults haven't been modified, and the cryptography chosen by the server -will still be whatever the default for your software was. For example, -if you obtain a certificate using *standalone* mode and then manually -install it in an IMAP or LDAP server, your cryptographic settings will -not be modified by the client in any way. +If you don't use Certbot to configure your server directly, because the +client doesn't integrate with your server software or because you chose +not to use this integration, then the cryptographic defaults haven't been +modified, and the cryptography chosen by the server will still be whatever +the default for your software was. For example, if you obtain a +certificate using *standalone* mode and then manually install it in an IMAP +or LDAP server, your cryptographic settings will not be modified by the +client in any way. Sources of defaults ------------------- -Initially, the Let's Encrypt client will configure users' servers to -use the cryptographic defaults recommended by the Mozilla project. -These settings are well-reasoned recommendations that carefully -consider client software compatibility. They are described at +Initially, Certbot will configure users' servers to use the cryptographic +defaults recommended by the Mozilla project. These settings are well-reasoned +recommendations that carefully consider client software compatibility. They +are described at https://wiki.mozilla.org/Security/Server_Side_TLS -and the version implemented by the Let's Encrypt client will be the +and the version implemented by Certbot will be the version that was most current as of the release date of each client version. Mozilla offers three separate sets of cryptographic options, which trade off security and compatibility differently. These are @@ -113,12 +110,12 @@ to most-backwards compatible). The client will follow the Mozilla defaults for the *Intermediate* configuration by default, at least with regards to ciphersuites and TLS versions. Mozilla's web site describes which client software will be compatible with each configuration. You can also use -the Qualys SSL Labs site, which the Let's Encrypt software will suggest +the Qualys SSL Labs site, which Certbot will suggest when installing a certificate, to test your server and see whether it will be compatible with particular software versions. -It will be possible to ask the Let's Encrypt client to instead apply -(and track) Modern or Old configurations. +It will be possible to ask Certbot to instead apply (and track) Modern +or Old configurations. The Let's Encrypt project expects to follow the Mozilla recommendations in the future as those recommendations are updated. (For example, some @@ -127,15 +124,15 @@ which uses the ChaCha and Poly1305 algorithms, and which is already implemented by the Chrome browser. Mozilla has delayed recommending ``0xcc13`` over compatibility and standardization concerns, but is likely to recommend it in the future once these concerns have been addressed. At -that point, the Let's Encrypt client would likely follow the Mozilla -recommendations and favor the use of this ciphersuite as well.) +that point, Certbot would likely follow the Mozilla recommendations and favor +the use of this ciphersuite as well.) The Let's Encrypt project may deviate from the Mozilla recommendations in the future if good cause is shown and we believe our users' priorities would be well-served by doing so. In general, please address relevant proposals for changing priorities to the Mozilla security -team first, before asking the Let's Encrypt project to change the -client's priorities. The Mozilla security team is likely to have more +team first, before asking the Let's Encrypt project to change +Certbot's priorities. The Mozilla security team is likely to have more resources and expertise to bring to bear on evaluating reasons why its recommendations should be updated. @@ -144,8 +141,8 @@ small number of alternative configurations (apart from Modern, Intermediate, and Old) that there's reason to believe would be widely used by sysadmins; this would usually be a preferable course to modifying an existing configuration. For example, if many sysadmins want their -servers configured to track a different expert recommendation, Let's -Encrypt could add an option to do so. +servers configured to track a different expert recommendation, Certbot +could add an option to do so. Resources for recommendations @@ -156,9 +153,9 @@ recommendations with sources of expert guidance on ciphersuites and other cryptographic parameters. We're grateful to everyone who contributed suggestions. The recommendations we received are available at -https://github.com/letsencrypt/letsencrypt/wiki/Ciphersuite-guidance +https://github.com/certbot/certbot/wiki/Ciphersuite-guidance -Let's Encrypt client users are welcome to review these authorities to +Certbot users are welcome to review these authorities to better inform their own cryptographic parameter choices. We also welcome suggestions of other resources to add to this list. Please keep in mind that different recommendations may reflect different priorities @@ -172,23 +169,22 @@ This will probably look something like .. code-block:: shell - letsencrypt --cipher-recommendations mozilla-secure - letsencrypt --cipher-recommendations mozilla-intermediate - letsencrypt --cipher-recommendations mozilla-old + certbot --cipher-recommendations mozilla-secure + certbot --cipher-recommendations mozilla-intermediate + certbot --cipher-recommendations mozilla-old to track Mozilla's *Secure*, *Intermediate*, or *Old* recommendations, and .. code-block:: shell - letsencrypt --update-ciphers on + certbot --update-ciphers on -to enable updating ciphers with each new Let's Encrypt client release, -or +to enable updating ciphers with each new Certbot release, or .. code-block:: shell - letsencrypt --update-ciphers off + certbot --update-ciphers off to disable automatic configuration updates. These features have not yet been implemented and this syntax may change then they are implemented. @@ -200,7 +196,7 @@ TODO The status of this feature is tracked as part of issue #1123 in our bug tracker. -https://github.com/letsencrypt/letsencrypt/issues/1123 +https://github.com/certbot/certbot/issues/1123 Prior to implementation of #1123, the client does not actually modify ciphersuites (this is intended to be implemented as a "configuration diff --git a/docs/contributing.rst b/docs/contributing.rst index 69604780c..5a9afd5c5 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -15,14 +15,14 @@ Running a local copy of the client ---------------------------------- Running the client in developer mode from your local tree is a little -different than running ``letsencrypt-auto``. To get set up, do these things +different than running ``certbot-auto``. To get set up, do these things once: .. code-block:: shell - git clone https://github.com/letsencrypt/letsencrypt - cd letsencrypt - ./letsencrypt-auto-source/letsencrypt-auto --os-packages-only + git clone https://github.com/certbot/certbot + cd certbot + ./certbot-auto-source/certbot-auto --os-packages-only ./tools/venv.sh Then in each shell where you're working on the client, do: @@ -36,7 +36,7 @@ client by typing: .. code-block:: shell - letsencrypt + certbot Activating a shell in this way makes it easier to run unit tests with ``tox`` and integration tests, as described below. To reverse this, you @@ -57,8 +57,8 @@ your pull request must have thorough unit test coverage, pass our `integration`_ tests, and be compliant with the :ref:`coding style `. -.. _github issue tracker: https://github.com/letsencrypt/letsencrypt/issues -.. _Good Volunteer Task: https://github.com/letsencrypt/letsencrypt/issues?q=is%3Aopen+is%3Aissue+label%3A%22Good+Volunteer+Task%22 +.. _github issue tracker: https://github.com/certbot/certbot/issues +.. _Good Volunteer Task: https://github.com/certbot/certbot/issues?q=is%3Aopen+is%3Aissue+label%3A%22Good+Volunteer+Task%22 Testing ------- @@ -97,7 +97,7 @@ Generally it is sufficient to open a pull request and let Github and Travis run integration tests for you. However, if you prefer to run tests, you can use Vagrant, using the Vagrantfile -in Let's Encrypt's repository. To execute the tests on a Vagrant box, the only +in Certbot's repository. To execute the tests on a Vagrant box, the only command you are required to run is:: ./tests/boulder-integration.sh @@ -141,12 +141,12 @@ and ``nginx.wtf`` to 127.0.0.1. You may now run (in a separate terminal):: ./tests/boulder-integration.sh && echo OK || echo FAIL -If you would like to test `letsencrypt_nginx` plugin (highly +If you would like to test `certbot_nginx` plugin (highly encouraged) make sure to install prerequisites as listed in -``letsencrypt-nginx/tests/boulder-integration.sh`` and rerun +``certbot-nginx/tests/boulder-integration.sh`` and rerun the integration tests suite. -.. _Boulder: https://github.com/letsencrypt/boulder +.. _Boulder: https://github.com/certbot/boulder .. _Go: https://golang.org @@ -155,28 +155,28 @@ Code components and layout acme contains all protocol specific code -letsencrypt +certbot all client code Plugin-architecture ------------------- -Let's Encrypt has a plugin architecture to facilitate support for +Certbot has a plugin architecture to facilitate support for different webservers, other TLS servers, and operating systems. The interfaces available for plugins to implement are defined in `interfaces.py`_ and `plugins/common.py`_. The most common kind of plugin is a "Configurator", which is likely to -implement the `~letsencrypt.interfaces.IAuthenticator` and -`~letsencrypt.interfaces.IInstaller` interfaces (though some +implement the `~certbot.interfaces.IAuthenticator` and +`~certbot.interfaces.IInstaller` interfaces (though some Configurators may implement just one of those). -There are also `~letsencrypt.interfaces.IDisplay` plugins, +There are also `~certbot.interfaces.IDisplay` plugins, which implement bindings to alternative UI libraries. -.. _interfaces.py: https://github.com/letsencrypt/letsencrypt/blob/master/letsencrypt/interfaces.py -.. _plugins/common.py: https://github.com/letsencrypt/letsencrypt/blob/master/letsencrypt/plugins/common.py#L34 +.. _interfaces.py: https://github.com/certbot/certbot/blob/master/certbot/interfaces.py +.. _plugins/common.py: https://github.com/certbot/certbot/blob/master/certbot/plugins/common.py#L34 Authenticators @@ -232,7 +232,7 @@ Installer Development --------------------- There are a few existing classes that may be beneficial while -developing a new `~letsencrypt.interfaces.IInstaller`. +developing a new `~certbot.interfaces.IInstaller`. Installers aimed to reconfigure UNIX servers may use Augeas for configuration parsing and can inherit from `~.AugeasConfigurator` class to handle much of the interface. Installers that are unable to use @@ -244,7 +244,7 @@ Display ~~~~~~~ We currently offer a pythondialog and "text" mode for displays. Display -plugins implement the `~letsencrypt.interfaces.IDisplay` +plugins implement the `~certbot.interfaces.IDisplay` interface. .. _dev-plugin: @@ -252,10 +252,10 @@ interface. Writing your own plugin ======================= -Let's Encrypt client supports dynamic discovery of plugins through the +Certbot supports dynamic discovery of plugins through the `setuptools entry points`_. This way you can, for example, create a -custom implementation of `~letsencrypt.interfaces.IAuthenticator` or -the `~letsencrypt.interfaces.IInstaller` without having to merge it +custom implementation of `~certbot.interfaces.IAuthenticator` or +the `~certbot.interfaces.IInstaller` without having to merge it with the core upstream source code. An example is provided in ``examples/plugins/`` directory. @@ -323,7 +323,7 @@ Steps: See `Known Issues`_. If it's not a known issue, fix any errors. .. _Known Issues: - https://github.com/letsencrypt/letsencrypt/wiki/Known-issues + https://github.com/certbot/certbot/wiki/Known-issues Updating the documentation ========================== @@ -345,7 +345,7 @@ Other methods for running the client Vagrant ------- -If you are a Vagrant user, Let's Encrypt comes with a Vagrantfile that +If you are a Vagrant user, Certbot comes with a Vagrantfile that automates setting up a development environment in an Ubuntu 14.04 LTS VM. To set it up, simply run ``vagrant up``. The repository is synced to ``/vagrant``, so you can get started with: @@ -354,7 +354,7 @@ synced to ``/vagrant``, so you can get started with: vagrant ssh cd /vagrant - sudo ./venv/bin/letsencrypt + sudo ./venv/bin/certbot Support for other Linux distributions coming soon. @@ -373,19 +373,19 @@ Docker ------ OSX users will probably find it easiest to set up a Docker container for -development. Let's Encrypt comes with a Dockerfile (``Dockerfile-dev``) +development. Certbot comes with a Dockerfile (``Dockerfile-dev``) for doing so. To use Docker on OSX, install and setup docker-machine using the instructions at https://docs.docker.com/installation/mac/. To build the development Docker image:: - docker build -t letsencrypt -f Dockerfile-dev . + docker build -t certbot -f Dockerfile-dev . Now run tests inside the Docker image: .. code-block:: shell - docker run -it letsencrypt bash + docker run -it certbot bash cd src tox -e py27 @@ -399,7 +399,7 @@ OS-level dependencies can be installed like so: .. code-block:: shell - letsencrypt-auto-source/letsencrypt-auto --os-packages-only + certbot-auto-source/certbot-auto --os-packages-only In general... diff --git a/docs/index.rst b/docs/index.rst index 68289d760..b541e376e 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,4 +1,4 @@ -Welcome to the Let's Encrypt client documentation! +Welcome to the Certbot documentation! ================================================== .. toctree:: diff --git a/docs/man/certbot.rst b/docs/man/certbot.rst new file mode 100644 index 000000000..7382d7811 --- /dev/null +++ b/docs/man/certbot.rst @@ -0,0 +1 @@ +.. program-output:: certbot --help all diff --git a/docs/man/letsencrypt.rst b/docs/man/letsencrypt.rst deleted file mode 100644 index 30f33c890..000000000 --- a/docs/man/letsencrypt.rst +++ /dev/null @@ -1 +0,0 @@ -.. program-output:: letsencrypt --help all diff --git a/docs/packaging.rst b/docs/packaging.rst index 5f09b65fa..bd366dbaa 100644 --- a/docs/packaging.rst +++ b/docs/packaging.rst @@ -3,4 +3,4 @@ Packaging Guide =============== Documentation can be found at -https://github.com/letsencrypt/letsencrypt/wiki/Packaging. +https://github.com/certbot/certbot/wiki/Packaging. diff --git a/docs/using.rst b/docs/using.rst index 66c5907ae..2b16e9a27 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -10,12 +10,12 @@ User Guide Installation ============ -.. _letsencrypt-auto: +.. _certbot-auto: -letsencrypt-auto +certbot-auto ---------------- -``letsencrypt-auto`` is a wrapper which installs some dependencies +``certbot-auto`` is a wrapper which installs some dependencies from your OS standard package repositories (e.g. using `apt-get` or `yum`), and for other dependencies it sets up a virtualized Python environment with packages downloaded from PyPI [#venv]_. It also @@ -25,33 +25,33 @@ To install and run the client, just type... .. code-block:: shell - ./letsencrypt-auto + ./certbot-auto -.. hint:: During the beta phase, Let's Encrypt enforces strict rate limits on +.. hint:: During the beta phase, Certbot enforces strict rate limits on the number of certificates issued for one domain. It is recommended to initially use the test server via `--test-cert` until you get the desired certificates. Throughout the documentation, whenever you see references to -``letsencrypt`` script/binary, you can substitute in -``letsencrypt-auto``. For example, to get basic help you would type: +``certbot`` script/binary, you can substitute in +``certbot-auto``. For example, to get basic help you would type: .. code-block:: shell - ./letsencrypt-auto --help + ./certbot-auto --help or for full help, type: .. code-block:: shell - ./letsencrypt-auto --help all + ./certbot-auto --help all -``letsencrypt-auto`` is the recommended method of running the Let's Encrypt +``certbot-auto`` is the recommended method of running the Certbot client beta releases on systems that don't have a packaged version. Debian, Arch Linux, Gentoo, FreeBSD, and OpenBSD now have native packages, so on those -systems you can just install ``letsencrypt`` (and perhaps -``letsencrypt-apache``). If you'd like to run the latest copy from Git, or +systems you can just install ``certbot`` (and perhaps +``certbot-apache``). If you'd like to run the latest copy from Git, or run your own locally modified copy of the client, follow the instructions in the :doc:`contributing`. Some `other methods of installation`_ are discussed below. @@ -60,11 +60,11 @@ below. Plugins ======= -The Let's Encrypt client supports a number of different "plugins" that can be +The Certbot client supports a number of different "plugins" that can be used to obtain and/or install certificates. Plugins that can obtain a cert are called "authenticators" and can be used with the "certonly" command. Plugins that can install a cert are called "installers". Plugins that do both -can be used with the "letsencrypt run" command, which is the default. +can be used with the "certbot run" command, which is the default. =========== ==== ==== =============================================================== Plugin Auth Inst Notes @@ -79,7 +79,7 @@ standalone_ Y N Uses a "standalone" webserver to obtain a cert. Requires webserver is not supported or not desired. 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_. +nginx_ Y Y Very experimental and not included in certbot-auto_. =========== ==== ==== =============================================================== There are also a number of third-party plugins for the client, provided by other developers: @@ -93,10 +93,10 @@ s3front_ Y Y Integration with Amazon CloudFront distribution of S3 buck gandi_ Y Y Integration with Gandi's hosting products and API =========== ==== ==== =============================================================== -.. _plesk: https://github.com/plesk/letsencrypt-plesk -.. _haproxy: https://code.greenhost.net/open/letsencrypt-haproxy -.. _s3front: https://github.com/dlapiduz/letsencrypt-s3front -.. _gandi: https://github.com/Gandi/letsencrypt-gandi +.. _plesk: https://github.com/plesk/certbot-plesk +.. _haproxy: https://code.greenhost.net/open/certbot-haproxy +.. _s3front: https://github.com/dlapiduz/certbot-s3front +.. _gandi: https://github.com/Gandi/certbot-gandi Future plugins for IMAP servers, SMTP servers, IRC servers, etc, are likely to be installers but not authenticators. @@ -130,21 +130,21 @@ specified ``--webroot-path``. So, for instance, :: - letsencrypt certonly --webroot -w /var/www/example/ -d www.example.com -d example.com -w /var/www/other -d other.example.net -d another.other.example.net + certbot certonly --webroot -w /var/www/example/ -d www.example.com -d example.com -w /var/www/other -d other.example.net -d another.other.example.net would obtain a single certificate for all of those names, using the ``/var/www/example`` webroot directory for the first two, and ``/var/www/other`` for the second two. The webroot plugin works by creating a temporary file for each of your requested -domains in ``${webroot-path}/.well-known/acme-challenge``. Then the Let's -Encrypt validation server makes HTTP requests to validate that the DNS for each -requested domain resolves to the server running letsencrypt. An example request +domains in ``${webroot-path}/.well-known/acme-challenge``. Then the Certbot +validation server makes HTTP requests to validate that the DNS for each +requested domain resolves to the server running certbot. An example request made to your web server would look like: :: - 66.133.109.36 - - [05/Jan/2016:20:11:24 -0500] "GET /.well-known/acme-challenge/HGr8U1IeTW4kY_Z6UIyaakzOkyQgPr_7ArlLgtZE8SX HTTP/1.1" 200 87 "-" "Mozilla/5.0 (compatible; Let's Encrypt validation server; +https://www.letsencrypt.org)" + 66.133.109.36 - - [05/Jan/2016:20:11:24 -0500] "GET /.well-known/acme-challenge/HGr8U1IeTW4kY_Z6UIyaakzOkyQgPr_7ArlLgtZE8SX HTTP/1.1" 200 87 "-" "Mozilla/5.0 (compatible; Certbot validation server; +https://www.certbot.com)" Note that to use the webroot plugin, your server must be configured to serve files from hidden directories. If ``/.well-known`` is treated specially by @@ -173,7 +173,7 @@ specified port using each requested domain name. Manual ------ -If you'd like to obtain a cert running ``letsencrypt`` on a machine +If you'd like to obtain a cert running ``certbot`` 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 @@ -187,14 +187,14 @@ 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 +certbot-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 +https://github.com/certbot/certbot/wiki/Plugins. If you're interested, you can also :ref:`write your own plugin `. Renewal @@ -204,11 +204,11 @@ Renewal days). Make sure you renew the certificates at least once in 3 months. -The ``letsencrypt`` client now supports a ``renew`` action to check +The ``certbot`` client now supports a ``renew`` action to check all installed certificates for impending expiry and attempt to renew them. The simplest form is simply -``letsencrypt renew`` +``certbot renew`` This will attempt to renew any previously-obtained certificates that expire in less than 30 days. The same plugin and options that were used @@ -229,9 +229,9 @@ certificate regardless of its age. (This form is not appropriate to run daily because each certificate will be renewed every day, which will quickly run into the certificate authority rate limit.) -Note that options provided to ``letsencrypt renew`` will apply to +Note that options provided to ``certbot renew`` will apply to *every* certificate for which renewal is attempted; for example, -``letsencrypt renew --rsa-key-size 4096`` would try to replace every +``certbot renew --rsa-key-size 4096`` would try to replace every near-expiry certificate with an equivalent certificate using a 4096-bit RSA public key. If a certificate is successfully renewed using specified options, those options will be saved and used for future @@ -240,10 +240,10 @@ renewals of that certificate. An alternative form that provides for more fine-grained control over the renewal process (while renewing specified certificates one at a time), -is ``letsencrypt certonly`` with the complete set of subject domains of +is ``certbot certonly`` with the complete set of subject domains of a specific certificate specified via `-d` flags, like -``letsencrypt certonly -d example.com -d www.example.com`` +``certbot certonly -d example.com -d www.example.com`` (All of the domains covered by the certificate must be specified in this case in order to renew and replace the old certificate rather @@ -256,7 +256,7 @@ The ``certonly`` form attempts to renew one individual certificate. Please note that the CA will send notification emails to the address you provide if you do not renew certificates that are about to expire. -Let's Encrypt is working hard on improving the renewal process, and we +Certbot is working hard on improving the renewal process, and we apologize for any inconveniences you encounter in integrating these commands into your individual environment. @@ -272,14 +272,14 @@ you prefer to manage everything by hand, this section provides information on where to find necessary files. All generated keys and issued certificates can be found in -``/etc/letsencrypt/live/$domain``. Rather than copying, please point +``/etc/certbot/live/$domain``. Rather than copying, please point your (web) server configuration directly to those files (or create -symlinks). During the renewal_, ``/etc/letsencrypt/live`` is updated +symlinks). During the renewal_, ``/etc/certbot/live`` is updated with the latest necessary files. -.. note:: ``/etc/letsencrypt/archive`` and ``/etc/letsencrypt/keys`` +.. note:: ``/etc/certbot/archive`` and ``/etc/certbot/keys`` contain all previous keys and certificates, while - ``/etc/letsencrypt/live`` symlinks to the latest versions. + ``/etc/certbot/live`` symlinks to the latest versions. The following files are available: @@ -287,7 +287,7 @@ The following files are available: Private key for the certificate. .. warning:: This **must be kept secret at all times**! Never share - it with anyone, including Let's Encrypt developers. You cannot + it with anyone, including Certbot developers. You cannot put it into a safe, however - your server still needs to access this file in order for SSL/TLS to work. @@ -340,7 +340,7 @@ Configuration file ================== It is possible to specify configuration file with -``letsencrypt-auto --config cli.ini`` (or shorter ``-c cli.ini``). An +``certbot-auto --config cli.ini`` (or shorter ``-c cli.ini``). An example configuration file is shown below: .. include:: ../examples/cli.ini @@ -348,9 +348,9 @@ example configuration file is shown below: By default, the following locations are searched: -- ``/etc/letsencrypt/cli.ini`` -- ``$XDG_CONFIG_HOME/letsencrypt/cli.ini`` (or - ``~/.config/letsencrypt/cli.ini`` if ``$XDG_CONFIG_HOME`` is not +- ``/etc/certbot/cli.ini`` +- ``$XDG_CONFIG_HOME/certbot/cli.ini`` (or + ``~/.config/certbot/cli.ini`` if ``$XDG_CONFIG_HOME`` is not set). .. keep it up to date with constants.py @@ -359,21 +359,21 @@ By default, the following locations are searched: Getting help ============ -If you're having problems you can chat with us on `IRC (#letsencrypt @ -Freenode) `_ or -get support on our `forums `_. +If you're having problems you can chat with us on `IRC (#certbot @ +OFTC) `_ or +get support on our `forums `_. If you find a bug in the software, please do report it in our `issue tracker -`_. Remember to +`_. Remember to give us as much information as possible: - copy and paste exact command line used and the output (though mind that the latter might include some personally identifiable information, including your email and domains) -- copy and paste logs from ``/var/log/letsencrypt`` (though mind they +- copy and paste logs from ``/var/log/certbot`` (though mind they also might contain personally identifiable information) -- copy and paste ``letsencrypt --version`` output +- copy and paste ``certbot --version`` output - your operating system, including specific version - specify which installation_ method you've chosen @@ -390,10 +390,10 @@ plugins cannot reach it from inside the Docker container. You should definitely read the :ref:`where-certs` section, in order to know how to manage the certs -manually. https://github.com/letsencrypt/letsencrypt/wiki/Ciphersuite-guidance +manually. https://github.com/certbot/certbot/wiki/Ciphersuite-guidance provides some information about recommended ciphersuites. If none of these make much sense to you, you should definitely use the -letsencrypt-auto_ method, which enables you to use installer plugins +certbot-auto_ method, which enables you to use installer plugins that cover both of those hard topics. If you're still not convinced and have decided to use this method, @@ -402,14 +402,14 @@ to, `install Docker`_, then issue the following command: .. code-block:: shell - sudo docker run -it --rm -p 443:443 -p 80:80 --name letsencrypt \ - -v "/etc/letsencrypt:/etc/letsencrypt" \ - -v "/var/lib/letsencrypt:/var/lib/letsencrypt" \ - quay.io/letsencrypt/letsencrypt:latest auth + sudo docker run -it --rm -p 443:443 -p 80:80 --name certbot \ + -v "/etc/certbot:/etc/certbot" \ + -v "/var/lib/certbot:/var/lib/certbot" \ + quay.io/certbot/certbot:latest auth and follow the instructions (note that ``auth`` command is explicitly used - no installer plugins involved). Your new cert will be available -in ``/etc/letsencrypt/live`` on the host. +in ``/etc/certbot/live`` on the host. .. _Docker: https://docker.com .. _`install Docker`: https://docs.docker.com/userguide/ @@ -420,31 +420,31 @@ Operating System Packages **FreeBSD** - * Port: ``cd /usr/ports/security/py-letsencrypt && make install clean`` - * Package: ``pkg install py27-letsencrypt`` + * Port: ``cd /usr/ports/security/py-certbot make install clean`` + * Package: ``pkg install py27-certbot`` **OpenBSD** - * Port: ``cd /usr/ports/security/letsencrypt/client && make install clean`` - * Package: ``pkg_add letsencrypt`` + * Port: ``cd /usr/ports/security/certbot/client && make install clean`` + * Package: ``pkg_add certbot`` **Arch Linux** .. code-block:: shell - sudo pacman -S letsencrypt letsencrypt-apache + sudo pacman -S certbot certbot-apache **Debian** -If you run Debian Stretch or Debian Sid, you can install letsencrypt packages. +If you run Debian Stretch or Debian Sid, you can install certbot packages. .. code-block:: shell sudo apt-get update - sudo apt-get install letsencrypt python-letsencrypt-apache + sudo apt-get install certbot python-certbot-apache If you don't want to use the Apache plugin, you can omit the -``python-letsencrypt-apache`` package. +``python-certbot-apache`` package. Packages for Debian Jessie are coming in the next few weeks. @@ -452,17 +452,17 @@ Packages for Debian Jessie are coming in the next few weeks. .. code-block:: shell - sudo dnf install letsencrypt + sudo dnf install certbot **Gentoo** -The official Let's Encrypt client is available in Gentoo Portage. If you +The official Certbot client is available in Gentoo Portage. If you want to use the Apache plugin, it has to be installed separately: .. code-block:: shell - emerge -av app-crypt/letsencrypt - emerge -av app-crypt/letsencrypt-apache + emerge -av app-crypt/certbot + emerge -av app-crypt/certbot-apache Currently, only the Apache plugin is included in Portage. However, if you want the nginx plugin, you can use Layman to add the mrueg overlay which @@ -473,7 +473,7 @@ does include the nginx plugin package: emerge -av app-portage/layman layman -S layman -a mrueg - emerge -av app-crypt/letsencrypt-nginx + emerge -av app-crypt/certbot-nginx When using the Apache plugin, you will run into a "cannot find a cert or key directive" error if you're sporting the default Gentoo ``httpd.conf``. @@ -503,7 +503,7 @@ Note: this change is not required for the other plugins. **Other Operating Systems** OS packaging is an ongoing effort. If you'd like to package -Let's Encrypt client for your distribution of choice please have a +Certbot for your distribution of choice please have a look at the :doc:`packaging`. @@ -519,19 +519,19 @@ whole process is described in the :doc:`contributing`. environment, e.g. ``sudo python setup.py install``, ``sudo pip install``, ``sudo ./venv/bin/...``. These modes of operation might corrupt your operating system and are **not supported** by the - Let's Encrypt team! + Certbot team! Comparison of different methods ------------------------------- Unless you have a very specific requirements, we kindly ask you to use -the letsencrypt-auto_ method. It's the fastest, the most thoroughly +the certbot-auto_ method. It's the fastest, the most thoroughly tested and the most reliable way of getting our software and the free SSL certificates! Beyond the methods discussed here, other methods may be possible, such as -installing Let's Encrypt directly with pip from PyPI or downloading a ZIP +installing Certbot directly with pip from PyPI or downloading a ZIP archive from GitHub may be technically possible but are not presently recommended or supported. From b8ea2c19a38d870556496b980de2472b4e369d32 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Wed, 6 Apr 2016 16:57:52 -0700 Subject: [PATCH 008/396] Lintian bug fix --- letsencrypt/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index f2ad50d45..adcc32a5e 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -272,7 +272,7 @@ class HelpfulArgumentParser(object): # Do any post-parsing homework here if self.verb == "renew": - self.noninteractive_mode = True + parsed_args.noninteractive_mode = True # we get domains from -d, but also from the webroot map... if parsed_args.webroot_map: From 0bcc80756d16de6aed3365b0fa3d27c2e2e98855 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Wed, 6 Apr 2016 17:10:30 -0700 Subject: [PATCH 009/396] Refactor config.server complexity out of parse_args --- letsencrypt/cli.py | 43 +++++++++++++++++++++++++------------------ 1 file changed, 25 insertions(+), 18 deletions(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index 8c2cd839a..75c7e116f 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -318,24 +318,7 @@ class HelpfulArgumentParser(object): parsed_args.noninteractive_mode = True if parsed_args.staging or parsed_args.dry_run: - if parsed_args.server not in (flag_default("server"), constants.STAGING_URI): - conflicts = ["--staging"] if parsed_args.staging else [] - conflicts += ["--dry-run"] if parsed_args.dry_run else [] - raise errors.Error("--server value conflicts with {0}".format( - " and ".join(conflicts))) - - parsed_args.server = constants.STAGING_URI - - if parsed_args.dry_run: - if self.verb not in ["certonly", "renew"]: - raise errors.Error("--dry-run currently only works with the " - "'certonly' or 'renew' subcommands (%r)" % self.verb) - parsed_args.break_my_certs = parsed_args.staging = True - if glob.glob(os.path.join(parsed_args.config_dir, constants.ACCOUNTS_DIR, "*")): - # The user has a prod account, but might not have a staging - # one; we don't want to start trying to perform interactive registration - parsed_args.tos = True - parsed_args.register_unsafely_without_email = True + self.set_test_server(parsed_args) if parsed_args.csr: if parsed_args.allow_subset_of_names: @@ -347,6 +330,30 @@ class HelpfulArgumentParser(object): return parsed_args + + def set_test_server(self, parsed_args): + "We have --staging/--dry-run; perform sanity check and set config.server" + + if parsed_args.server not in (flag_default("server"), constants.STAGING_URI): + conflicts = ["--staging"] if parsed_args.staging else [] + conflicts += ["--dry-run"] if parsed_args.dry_run else [] + raise errors.Error("--server value conflicts with {0}".format( + " and ".join(conflicts))) + + parsed_args.server = constants.STAGING_URI + + if parsed_args.dry_run: + if self.verb not in ["certonly", "renew"]: + raise errors.Error("--dry-run currently only works with the " + "'certonly' or 'renew' subcommands (%r)" % self.verb) + parsed_args.break_my_certs = parsed_args.staging = True + if glob.glob(os.path.join(parsed_args.config_dir, constants.ACCOUNTS_DIR, "*")): + # The user has a prod account, but might not have a staging + # one; we don't want to start trying to perform interactive registration + parsed_args.tos = True + parsed_args.register_unsafely_without_email = True + + def handle_csr(self, parsed_args): """Process a --csr flag.""" if parsed_args.verb != "certonly": From 5e3fc3a957eaaff225e21916b1241cd5a37ec522 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Wed, 6 Apr 2016 17:14:29 -0700 Subject: [PATCH 010/396] Keep all --csr checks in HelpfulArgumentParser.handle_csr --- letsencrypt/cli.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index 75c7e116f..74c51f5e3 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -321,9 +321,6 @@ class HelpfulArgumentParser(object): self.set_test_server(parsed_args) if parsed_args.csr: - if parsed_args.allow_subset_of_names: - raise errors.Error("--allow-subset-of-names " - "cannot be used with --csr") self.handle_csr(parsed_args) hooks.validate_hooks(parsed_args) @@ -361,6 +358,8 @@ class HelpfulArgumentParser(object): "when obtaining a new or replacement " "via the certonly command. Please try the " "certonly command instead.") + if parsed_args.allow_subset_of_names: + raise errors.Error("--allow-subset-of-names cannot be used with --csr") try: csr = le_util.CSR(file=parsed_args.csr[0], data=parsed_args.csr[1], form="der") From 526bc5cf84aeaca0669d87ada3ced32615efe8e9 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Wed, 6 Apr 2016 17:35:29 -0700 Subject: [PATCH 011/396] Refactor CSR importing from cli -> crypto_util More specifically: HelpfulArgumentParser.handle_csr -> crypto_util.import_csr_file --- letsencrypt/cli.py | 18 ++---------------- letsencrypt/crypto_util.py | 30 ++++++++++++++++++++++++++++++ letsencrypt/tests/client_test.py | 6 ++++-- 3 files changed, 36 insertions(+), 18 deletions(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index 74c51f5e3..61fc57777 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -6,10 +6,8 @@ import logging import logging.handlers import os import sys -import traceback import configargparse -import OpenSSL import six import letsencrypt @@ -361,20 +359,8 @@ class HelpfulArgumentParser(object): if parsed_args.allow_subset_of_names: raise errors.Error("--allow-subset-of-names cannot be used with --csr") - try: - csr = le_util.CSR(file=parsed_args.csr[0], data=parsed_args.csr[1], form="der") - typ = OpenSSL.crypto.FILETYPE_ASN1 - domains = crypto_util.get_sans_from_csr(csr.data, OpenSSL.crypto.FILETYPE_ASN1) - except OpenSSL.crypto.Error: - try: - e1 = traceback.format_exc() - typ = OpenSSL.crypto.FILETYPE_PEM - csr = le_util.CSR(file=parsed_args.csr[0], data=parsed_args.csr[1], form="pem") - domains = crypto_util.get_sans_from_csr(csr.data, typ) - except OpenSSL.crypto.Error: - logger.debug("DER CSR parse error %s", e1) - logger.debug("PEM CSR parse error %s", traceback.format_exc()) - raise errors.Error("Failed to parse CSR file: {0}".format(parsed_args.csr[0])) + csrfile, contents = parsed_args.csr[0:2] + typ, csr, domains = crypto_util.import_csr_file(csrfile, contents) # This is not necessary for webroot to work, however, # obtain_certificate_from_csr requires parsed_args.domains to be set diff --git a/letsencrypt/crypto_util.py b/letsencrypt/crypto_util.py index 5fdcba843..2ca43f76f 100644 --- a/letsencrypt/crypto_util.py +++ b/letsencrypt/crypto_util.py @@ -6,6 +6,7 @@ """ import logging import os +import traceback import OpenSSL import pyrfc3339 @@ -171,6 +172,35 @@ def csr_matches_pubkey(csr, privkey): return False +def import_csr_file(csrfile, contents): + """Import a CSR file, which can be either PEM or DER. + + :param str csrfile: CSR filename + :param str contents: contens of the CSR file + + :rtype: tuple + + :returns: (le_util.CSR object representing the CSR, + OpenSSL FILETYPE_ representing DER or PEM, + list of domains requested in the CSR) + """ + try: + csr = le_util.CSR(file=csrfile, data=contents, form="der") + typ = OpenSSL.crypto.FILETYPE_ASN1 + domains = get_sans_from_csr(csr.data, OpenSSL.crypto.FILETYPE_ASN1) + except OpenSSL.crypto.Error: + try: + e1 = traceback.format_exc() + typ = OpenSSL.crypto.FILETYPE_PEM + csr = le_util.CSR(file=csrfile, data=contents, form="pem") + domains = get_sans_from_csr(csr.data, typ) + except OpenSSL.crypto.Error: + logger.debug("DER CSR parse error %s", e1) + logger.debug("PEM CSR parse error %s", traceback.format_exc()) + raise errors.Error("Failed to parse CSR file: {0}".format(csrfile)) + return typ, csr, domains + + def make_key(bits): """Generate PEM encoded RSA key. diff --git a/letsencrypt/tests/client_test.py b/letsencrypt/tests/client_test.py index cd6b11158..c3d5b322f 100644 --- a/letsencrypt/tests/client_test.py +++ b/letsencrypt/tests/client_test.py @@ -134,7 +134,7 @@ class ClientTest(unittest.TestCase): self.acme.fetch_chain.assert_called_once_with(mock.sentinel.certr) - # FIXME move parts of this to test_cli.py... + # FIXME move parts of this to crypto_util tests... @mock.patch("letsencrypt.client.logger") def test_obtain_certificate_from_csr(self, mock_logger): self._mock_obtain_certificate() @@ -144,9 +144,11 @@ class ClientTest(unittest.TestCase): # The CLI should believe that this is a certonly request, because # a CSR would not be allowed with other kinds of requests! mock_parsed_args.verb = "certonly" - with mock.patch("letsencrypt.client.le_util.CSR") as mock_CSR: + with mock.patch("letsencrypt.cli.crypto_util.le_util.CSR") as mock_CSR: mock_CSR.return_value = test_csr mock_parsed_args.domains = self.eg_domains[:] + mock_parsed_args.allow_subset_of_names = False + mock_parsed_args.csr = (mock.MagicMock(), mock.MagicMock()) mock_parser = mock.MagicMock(cli.HelpfulArgumentParser) cli.HelpfulArgumentParser.handle_csr(mock_parser, mock_parsed_args) From c4974e6d937eec5dd85b8fd99c6f477594cc6379 Mon Sep 17 00:00:00 2001 From: Pavel Pavlov Date: Thu, 7 Apr 2016 18:11:55 +0300 Subject: [PATCH 012/396] Nginx map statement hotfix --- letsencrypt-nginx/letsencrypt_nginx/nginxparser.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/letsencrypt-nginx/letsencrypt_nginx/nginxparser.py b/letsencrypt-nginx/letsencrypt_nginx/nginxparser.py index cef0756d7..69594efc3 100644 --- a/letsencrypt-nginx/letsencrypt_nginx/nginxparser.py +++ b/letsencrypt-nginx/letsencrypt_nginx/nginxparser.py @@ -30,10 +30,11 @@ class RawNginxParser(object): assignment = (key + Optional(space + value, default=None) + semicolon) location_statement = Optional(space + modifier) + Optional(space + location) if_statement = Literal("if") + space + Regex(r"\(.+\)") + space + map_statement = Literal("map") + space + Regex(r"\S+") + space + Regex(r"\$\S+") + space block = Forward() block << Group( - (Group(key + location_statement) ^ Group(if_statement)) + + (Group(key + location_statement) ^ Group(if_statement) ^ Group(map_statement)) + left_bracket + Group(ZeroOrMore(Group(comment | assignment) | block)) + right_bracket) From 8145b7c11b9f824831ef2f059eaafdb664307d90 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Sat, 9 Apr 2016 08:15:24 +0000 Subject: [PATCH 013/396] ACME: omitempty Error.detail, Error.type (fixes #2289) --- acme/acme/messages.py | 4 ++-- acme/acme/messages_test.py | 8 ++++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/acme/acme/messages.py b/acme/acme/messages.py index 24a3b580c..40ddbdb2f 100644 --- a/acme/acme/messages.py +++ b/acme/acme/messages.py @@ -37,9 +37,9 @@ class Error(jose.JSONObjectWithFields, errors.Error): ) ) - typ = jose.Field('type') + typ = jose.Field('type', omitempty=True, default='about:blank') title = jose.Field('title', omitempty=True) - detail = jose.Field('detail') + detail = jose.Field('detail', omitempty=True) @property def description(self): diff --git a/acme/acme/messages_test.py b/acme/acme/messages_test.py index b2b7febdc..c1f027302 100644 --- a/acme/acme/messages_test.py +++ b/acme/acme/messages_test.py @@ -28,6 +28,14 @@ class ErrorTest(unittest.TestCase): self.error_custom = Error(typ='custom', detail='bar') self.jobj_cusom = {'type': 'custom', 'detail': 'bar'} + def test_default_typ(self): + from acme.messages import Error + self.assertEqual(Error().typ, 'about:blank') + + def test_from_json_empty(self): + from acme.messages import Error + self.assertEqual(Error(), Error.from_json('{}')) + def test_from_json_hashable(self): from acme.messages import Error hash(Error.from_json(self.error.to_json())) From 0839168de7da3950e93056c2f80d029e7225e43f Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Sun, 10 Apr 2016 07:50:39 +0000 Subject: [PATCH 014/396] Fake deserialization error in test_check_response_not_ok_jobj_no_error --- acme/acme/client_test.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/acme/acme/client_test.py b/acme/acme/client_test.py index c8e95a423..7403cde81 100644 --- a/acme/acme/client_test.py +++ b/acme/acme/client_test.py @@ -484,9 +484,11 @@ class ClientNetworkTest(unittest.TestCase): def test_check_response_not_ok_jobj_no_error(self): self.response.ok = False self.response.json.return_value = {} - # pylint: disable=protected-access - self.assertRaises( - errors.ClientError, self.net._check_response, self.response) + with mock.patch('acme.client.messages.Error.from_json') as from_json: + from_json.side_effect = jose.DeserializationError + # pylint: disable=protected-access + self.assertRaises( + errors.ClientError, self.net._check_response, self.response) def test_check_response_not_ok_jobj_error(self): self.response.ok = False From c82a551e77faa228da0a9e445cd0eb7f0417537d Mon Sep 17 00:00:00 2001 From: Joona Hoikkala Date: Tue, 12 Apr 2016 00:03:48 +0300 Subject: [PATCH 015/396] os-release parsing WIP --- letsencrypt/le_util.py | 48 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/letsencrypt/le_util.py b/letsencrypt/le_util.py index cb1c61074..7ee5e33db 100644 --- a/letsencrypt/le_util.py +++ b/letsencrypt/le_util.py @@ -210,8 +210,56 @@ def safely_remove(path): def get_os_info(): + """ + Get OS name and version + + :returns: (os_name, os_version) + :rtype: `tuple` of `str` + """ + + +def get_systemd_os_info(): + """ + Parse systemd /etc/os-release for distribution information + + :returns: (os_name, os_version) + :rtype: `tuple` of `str` + """ + + os_name = _get_systemd_os_release_var("ID") + os_version = _get_systemd_os_release_var("VERSION_ID") + + return (os_name, os_version) + + +def _get_systemd_os_release_var(varname): + + OS_RELEASE_FILEPATH = "/etc/os-release" + var_string = varname+"=" + if not os.path.isfile(OS_RELEASE_FILEPATH): + return "" + with open(OS_RELEASE_FILEPATH, 'r') as fh: + contents = fh.readlines() + + for line in contents: + if line.strip().startswith(var_string): + # Return the value of var, normalized + return _normalize_string(line.strip()[len(var_string):]) + return "" + + +def _normalize_string(orig): + """ + Helper function for _get_systemd_os_release_var() to remove quotes + and whitespaces + """ + return orig.replace('"', '').replace("'", "").strip() + + +def get_python_os_info(): """ Get Operating System type/distribution and major version + using python platform module :returns: (os_name, os_version) :rtype: `tuple` of `str` From bbb300eb229bcca0938b50fef9421f02b3b6d88b Mon Sep 17 00:00:00 2001 From: Joona Hoikkala Date: Tue, 12 Apr 2016 14:27:00 +0300 Subject: [PATCH 016/396] Finalized parsing and fixed test case --- letsencrypt/le_util.py | 16 ++++++++++++++++ letsencrypt/tests/cli_test.py | 2 +- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/letsencrypt/le_util.py b/letsencrypt/le_util.py index 7ee5e33db..60cdd0314 100644 --- a/letsencrypt/le_util.py +++ b/letsencrypt/le_util.py @@ -217,6 +217,15 @@ def get_os_info(): :rtype: `tuple` of `str` """ + if os.path.isfile('/etc/os-release'): + # Systemd os-release parsing might be viable + os_name, os_version = get_systemd_os_info() + if os_name: + return (os_name, os_version) + + # Fallback to platform module + return get_python_os_info() + def get_systemd_os_info(): """ @@ -233,6 +242,13 @@ def get_systemd_os_info(): def _get_systemd_os_release_var(varname): + """ + Get single value from systemd /etc/os-release + + :param str varname: Name of variable to fetch + :returns: requested value + :rtype: `str` + """ OS_RELEASE_FILEPATH = "/etc/os-release" var_string = varname+"=" diff --git a/letsencrypt/tests/cli_test.py b/letsencrypt/tests/cli_test.py index eb3f48308..03ad45514 100644 --- a/letsencrypt/tests/cli_test.py +++ b/letsencrypt/tests/cli_test.py @@ -169,7 +169,7 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods import platform plat = platform.platform() if "linux" in plat.lower(): - self.assertTrue(platform.linux_distribution()[0] in ua) + self.assertTrue(le_util.get_os_info()[0] in ua) with mock.patch('letsencrypt.main.client.acme_client.ClientNetwork') as acme_net: ua = "bandersnatch" From 7ff8440b8f49d527f3a61055aa8fb311ca90063b Mon Sep 17 00:00:00 2001 From: Joona Hoikkala Date: Tue, 12 Apr 2016 18:12:47 +0300 Subject: [PATCH 017/396] Tests for systemd os-release. Fix for darwin OS version info and tests for it --- letsencrypt/le_util.py | 1 - letsencrypt/tests/le_util_test.py | 34 +++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/letsencrypt/le_util.py b/letsencrypt/le_util.py index 60cdd0314..71dff7575 100644 --- a/letsencrypt/le_util.py +++ b/letsencrypt/le_util.py @@ -300,7 +300,6 @@ def get_python_os_info(): ["sw_vers", "-productVersion"], stdout=subprocess.PIPE ).communicate()[0] - os_ver = os_ver.partition(".")[0] elif os_type.startswith('freebsd'): # eg "9.3-RC3-p1" os_ver = os_ver.partition("-")[0] diff --git a/letsencrypt/tests/le_util_test.py b/letsencrypt/tests/le_util_test.py index 0f9464c6f..e1770eaed 100644 --- a/letsencrypt/tests/le_util_test.py +++ b/letsencrypt/tests/le_util_test.py @@ -339,5 +339,39 @@ class EnforceDomainSanityTest(unittest.TestCase): u"eichh\u00f6rnchen.example.com") +class OsInfoTest(unittest.TestCase): + """Test OS / distribution detection""" + def _call(self): + from letsencrypt.le_util import get_os_info + return get_os_info() + + def test_systemd_os_release(self): + from letsencrypt.le_util import get_os_info + os_release = 'VERSION_ID=42\nID=doobian\n' + with mock.patch('__builtin__.open', + mock.mock_open(read_data=os_release)): + with mock.patch('os.path.isfile', return_value=True): + self.assertEqual(get_os_info()[0], 'doobian') + self.assertEqual(get_os_info()[1], '42') + + @mock.patch("letsencrypt.le_util.subprocess.Popen") + def test_non_systemd_os_info(self, popen_mock): + from letsencrypt.le_util import get_os_info + with mock.patch('os.path.isfile', return_value=False): + with mock.patch('platform.system_alias', + return_value=('NonSystemD','42','42')): + self.assertEqual(get_os_info()[0], 'nonsystemd') + + with mock.patch('platform.system_alias', + return_value=('darwin', '', '')): + comm_mock = mock.Mock() + comm_attrs = {'communicate.return_value': + ('42.42.42', 'error')} + comm_mock.configure_mock(**comm_attrs) + popen_mock.return_value = comm_mock + self.assertEqual(get_os_info()[0], 'darwin') + self.assertEqual(get_os_info()[1], '42.42.42') + + if __name__ == "__main__": unittest.main() # pragma: no cover From 34f0e260f1ee7c900330d60f47b7e6a06aabd15b Mon Sep 17 00:00:00 2001 From: Joona Hoikkala Date: Tue, 12 Apr 2016 18:49:08 +0300 Subject: [PATCH 018/396] Linter fixes --- letsencrypt/tests/le_util_test.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/letsencrypt/tests/le_util_test.py b/letsencrypt/tests/le_util_test.py index e1770eaed..10d2c91ad 100644 --- a/letsencrypt/tests/le_util_test.py +++ b/letsencrypt/tests/le_util_test.py @@ -359,7 +359,7 @@ class OsInfoTest(unittest.TestCase): from letsencrypt.le_util import get_os_info with mock.patch('os.path.isfile', return_value=False): with mock.patch('platform.system_alias', - return_value=('NonSystemD','42','42')): + return_value=('NonSystemD', '42', '42')): self.assertEqual(get_os_info()[0], 'nonsystemd') with mock.patch('platform.system_alias', @@ -367,7 +367,7 @@ class OsInfoTest(unittest.TestCase): comm_mock = mock.Mock() comm_attrs = {'communicate.return_value': ('42.42.42', 'error')} - comm_mock.configure_mock(**comm_attrs) + comm_mock.configure_mock(**comm_attrs) # pylint disable=star-args popen_mock.return_value = comm_mock self.assertEqual(get_os_info()[0], 'darwin') self.assertEqual(get_os_info()[1], '42.42.42') From 57738142e28c6841aab9a909883ecf166e6b9560 Mon Sep 17 00:00:00 2001 From: Joona Hoikkala Date: Tue, 12 Apr 2016 18:59:27 +0300 Subject: [PATCH 019/396] Added constants for os-release names --- letsencrypt-apache/letsencrypt_apache/constants.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/letsencrypt-apache/letsencrypt_apache/constants.py b/letsencrypt-apache/letsencrypt_apache/constants.py index 8b502b4d8..72bbdca6d 100644 --- a/letsencrypt-apache/letsencrypt_apache/constants.py +++ b/letsencrypt-apache/letsencrypt_apache/constants.py @@ -78,6 +78,8 @@ CLI_DEFAULTS = { "centos linux": CLI_DEFAULTS_CENTOS, "fedora": CLI_DEFAULTS_CENTOS, "red hat enterprise linux server": CLI_DEFAULTS_CENTOS, + "rhel": CLI_DEFAULTS_CENTOS, + "amazon": CLI_DEFAULTS_CENTOS, "gentoo base system": CLI_DEFAULTS_GENTOO, "darwin": CLI_DEFAULTS_DARWIN, } From 67c60ab406e7b1b34637371e7392d8ce51889d82 Mon Sep 17 00:00:00 2001 From: Joona Hoikkala Date: Tue, 12 Apr 2016 19:41:39 +0300 Subject: [PATCH 020/396] Disabled linter error --- letsencrypt/tests/le_util_test.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/letsencrypt/tests/le_util_test.py b/letsencrypt/tests/le_util_test.py index 10d2c91ad..74b1bb703 100644 --- a/letsencrypt/tests/le_util_test.py +++ b/letsencrypt/tests/le_util_test.py @@ -347,11 +347,11 @@ class OsInfoTest(unittest.TestCase): def test_systemd_os_release(self): from letsencrypt.le_util import get_os_info - os_release = 'VERSION_ID=42\nID=doobian\n' + os_release = 'VERSION_ID=42\nID=systemdos\n' with mock.patch('__builtin__.open', mock.mock_open(read_data=os_release)): with mock.patch('os.path.isfile', return_value=True): - self.assertEqual(get_os_info()[0], 'doobian') + self.assertEqual(get_os_info()[0], 'systemdos') self.assertEqual(get_os_info()[1], '42') @mock.patch("letsencrypt.le_util.subprocess.Popen") @@ -367,7 +367,7 @@ class OsInfoTest(unittest.TestCase): comm_mock = mock.Mock() comm_attrs = {'communicate.return_value': ('42.42.42', 'error')} - comm_mock.configure_mock(**comm_attrs) # pylint disable=star-args + comm_mock.configure_mock(**comm_attrs) # pylint: disable=star-args popen_mock.return_value = comm_mock self.assertEqual(get_os_info()[0], 'darwin') self.assertEqual(get_os_info()[1], '42.42.42') From 6fc63de5a587c91f3ba87d51b4e38ef8d9f0b44a Mon Sep 17 00:00:00 2001 From: Joona Hoikkala Date: Wed, 13 Apr 2016 00:49:51 +0300 Subject: [PATCH 021/396] Using mocked os-release file --- letsencrypt/le_util.py | 22 ++++++++++++---------- letsencrypt/tests/le_util_test.py | 13 +++++++------ 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/letsencrypt/le_util.py b/letsencrypt/le_util.py index 71dff7575..927d8f2e8 100644 --- a/letsencrypt/le_util.py +++ b/letsencrypt/le_util.py @@ -209,17 +209,18 @@ def safely_remove(path): raise -def get_os_info(): +def get_os_info(filepath="/etc/os-release"): """ Get OS name and version + :param str filepath: File path of os-release file :returns: (os_name, os_version) :rtype: `tuple` of `str` """ - if os.path.isfile('/etc/os-release'): + if os.path.isfile(filepath): # Systemd os-release parsing might be viable - os_name, os_version = get_systemd_os_info() + os_name, os_version = get_systemd_os_info(filepath=filepath) if os_name: return (os_name, os_version) @@ -227,34 +228,35 @@ def get_os_info(): return get_python_os_info() -def get_systemd_os_info(): +def get_systemd_os_info(filepath="/etc/os-release"): """ Parse systemd /etc/os-release for distribution information + :param str filepath: File path of os-release file :returns: (os_name, os_version) :rtype: `tuple` of `str` """ - os_name = _get_systemd_os_release_var("ID") - os_version = _get_systemd_os_release_var("VERSION_ID") + os_name = _get_systemd_os_release_var("ID", filepath=filepath) + os_version = _get_systemd_os_release_var("VERSION_ID", filepath=filepath) return (os_name, os_version) -def _get_systemd_os_release_var(varname): +def _get_systemd_os_release_var(varname, filepath="/etc/os-release"): """ Get single value from systemd /etc/os-release :param str varname: Name of variable to fetch + :param str filepath: File path of os-release file :returns: requested value :rtype: `str` """ - OS_RELEASE_FILEPATH = "/etc/os-release" var_string = varname+"=" - if not os.path.isfile(OS_RELEASE_FILEPATH): + if not os.path.isfile(filepath): return "" - with open(OS_RELEASE_FILEPATH, 'r') as fh: + with open(filepath, 'r') as fh: contents = fh.readlines() for line in contents: diff --git a/letsencrypt/tests/le_util_test.py b/letsencrypt/tests/le_util_test.py index 74b1bb703..c43dbfe2c 100644 --- a/letsencrypt/tests/le_util_test.py +++ b/letsencrypt/tests/le_util_test.py @@ -4,6 +4,7 @@ import errno import os import shutil import stat +import sys import tempfile import unittest @@ -11,6 +12,7 @@ import mock import six from letsencrypt import errors +from letsencrypt.tests import test_util class RunScriptTest(unittest.TestCase): @@ -347,12 +349,11 @@ class OsInfoTest(unittest.TestCase): def test_systemd_os_release(self): from letsencrypt.le_util import get_os_info - os_release = 'VERSION_ID=42\nID=systemdos\n' - with mock.patch('__builtin__.open', - mock.mock_open(read_data=os_release)): - with mock.patch('os.path.isfile', return_value=True): - self.assertEqual(get_os_info()[0], 'systemdos') - self.assertEqual(get_os_info()[1], '42') + with mock.patch('os.path.isfile', return_value=True): + self.assertEqual(get_os_info( + test_util.vector_path("os-release"))[0], 'systemdos') + self.assertEqual(get_os_info( + test_util.vector_path("os-release"))[1], '42') @mock.patch("letsencrypt.le_util.subprocess.Popen") def test_non_systemd_os_info(self, popen_mock): From 608352157c3fb618e4e345509e0163481596dcba Mon Sep 17 00:00:00 2001 From: Joona Hoikkala Date: Wed, 13 Apr 2016 00:55:21 +0300 Subject: [PATCH 022/396] ..and the test file of course --- letsencrypt/tests/testdata/os-release | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 letsencrypt/tests/testdata/os-release diff --git a/letsencrypt/tests/testdata/os-release b/letsencrypt/tests/testdata/os-release new file mode 100644 index 000000000..b7c3ceb1b --- /dev/null +++ b/letsencrypt/tests/testdata/os-release @@ -0,0 +1,8 @@ +NAME="SystemdOS" +VERSION="42.42.42 LTS, Unreal" +ID=systemdos +ID_LIKE=debian +PRETTY_NAME="SystemdOS 42.42.42 Unreal" +VERSION_ID="42" +HOME_URL="http://www.example.com/" +SUPPORT_URL="http://help.example.com/" From 096873ca1c7d2c998283fa0cd3835db6bac3ea9c Mon Sep 17 00:00:00 2001 From: Joona Hoikkala Date: Wed, 13 Apr 2016 01:21:34 +0300 Subject: [PATCH 023/396] Removed unused import --- letsencrypt/tests/le_util_test.py | 1 - 1 file changed, 1 deletion(-) diff --git a/letsencrypt/tests/le_util_test.py b/letsencrypt/tests/le_util_test.py index c43dbfe2c..bab711ded 100644 --- a/letsencrypt/tests/le_util_test.py +++ b/letsencrypt/tests/le_util_test.py @@ -4,7 +4,6 @@ import errno import os import shutil import stat -import sys import tempfile import unittest From 995b22684f20a5de2f5b48dc34816f54f9af654c Mon Sep 17 00:00:00 2001 From: Joona Hoikkala Date: Thu, 14 Apr 2016 08:08:08 +0300 Subject: [PATCH 024/396] Removed unused test method --- letsencrypt/tests/le_util_test.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/letsencrypt/tests/le_util_test.py b/letsencrypt/tests/le_util_test.py index bab711ded..5187964a4 100644 --- a/letsencrypt/tests/le_util_test.py +++ b/letsencrypt/tests/le_util_test.py @@ -342,9 +342,6 @@ class EnforceDomainSanityTest(unittest.TestCase): class OsInfoTest(unittest.TestCase): """Test OS / distribution detection""" - def _call(self): - from letsencrypt.le_util import get_os_info - return get_os_info() def test_systemd_os_release(self): from letsencrypt.le_util import get_os_info From b1d7bd318e649474dd763667920e8e8744b961b9 Mon Sep 17 00:00:00 2001 From: Joona Hoikkala Date: Thu, 14 Apr 2016 08:39:39 +0300 Subject: [PATCH 025/396] Full test coverage for le_util and os detection --- letsencrypt/tests/le_util_test.py | 30 +++++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/letsencrypt/tests/le_util_test.py b/letsencrypt/tests/le_util_test.py index 5187964a4..435760828 100644 --- a/letsencrypt/tests/le_util_test.py +++ b/letsencrypt/tests/le_util_test.py @@ -344,16 +344,19 @@ class OsInfoTest(unittest.TestCase): """Test OS / distribution detection""" def test_systemd_os_release(self): - from letsencrypt.le_util import get_os_info + from letsencrypt.le_util import get_os_info, get_systemd_os_info with mock.patch('os.path.isfile', return_value=True): self.assertEqual(get_os_info( test_util.vector_path("os-release"))[0], 'systemdos') self.assertEqual(get_os_info( test_util.vector_path("os-release"))[1], '42') + self.assertEqual(get_systemd_os_info("/dev/null"), ("", "")) + with mock.patch('os.path.isfile', return_value=False): + self.assertEqual(get_systemd_os_info(), ("", "")) @mock.patch("letsencrypt.le_util.subprocess.Popen") def test_non_systemd_os_info(self, popen_mock): - from letsencrypt.le_util import get_os_info + from letsencrypt.le_util import get_os_info, get_python_os_info with mock.patch('os.path.isfile', return_value=False): with mock.patch('platform.system_alias', return_value=('NonSystemD', '42', '42')): @@ -364,11 +367,32 @@ class OsInfoTest(unittest.TestCase): comm_mock = mock.Mock() comm_attrs = {'communicate.return_value': ('42.42.42', 'error')} - comm_mock.configure_mock(**comm_attrs) # pylint: disable=star-args + comm_mock.configure_mock(**comm_attrs) # pylint: disable=star-args popen_mock.return_value = comm_mock self.assertEqual(get_os_info()[0], 'darwin') self.assertEqual(get_os_info()[1], '42.42.42') + with mock.patch('platform.system_alias', + return_value=('linux', '', '')): + with mock.patch('platform.linux_distribution', + return_value=('', '', '')): + self.assertEqual(get_python_os_info(), ("linux", "")) + + with mock.patch('platform.linux_distribution', + return_value=('testdist', '42', '')): + self.assertEqual(get_python_os_info(), ("testdist", "42")) + + with mock.patch('platform.system_alias', + return_value=('freebsd', '9.3-RC3-p1', '')): + self.assertEqual(get_python_os_info(), ("freebsd", "9")) + + with mock.patch('platform.system_alias', + return_value=('windows', '', '')): + with mock.patch('platform.win32_ver', + return_value=('4242', '95', '2', '')): + self.assertEqual(get_python_os_info(), + ("windows", "95")) + if __name__ == "__main__": unittest.main() # pragma: no cover From 7563d65cb324c5702f55593276e941e036e585b8 Mon Sep 17 00:00:00 2001 From: Joona Hoikkala Date: Sat, 16 Apr 2016 15:39:31 +0300 Subject: [PATCH 026/396] Name change for tests --- certbot/tests/le_util_test.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/certbot/tests/le_util_test.py b/certbot/tests/le_util_test.py index 752e66a63..23ea40987 100644 --- a/certbot/tests/le_util_test.py +++ b/certbot/tests/le_util_test.py @@ -344,7 +344,7 @@ class OsInfoTest(unittest.TestCase): """Test OS / distribution detection""" def test_systemd_os_release(self): - from letsencrypt.le_util import get_os_info, get_systemd_os_info + from certbot.le_util import get_os_info, get_systemd_os_info with mock.patch('os.path.isfile', return_value=True): self.assertEqual(get_os_info( test_util.vector_path("os-release"))[0], 'systemdos') @@ -354,9 +354,9 @@ class OsInfoTest(unittest.TestCase): with mock.patch('os.path.isfile', return_value=False): self.assertEqual(get_systemd_os_info(), ("", "")) - @mock.patch("letsencrypt.le_util.subprocess.Popen") + @mock.patch("certbot.le_util.subprocess.Popen") def test_non_systemd_os_info(self, popen_mock): - from letsencrypt.le_util import get_os_info, get_python_os_info + from certbot.le_util import get_os_info, get_python_os_info with mock.patch('os.path.isfile', return_value=False): with mock.patch('platform.system_alias', return_value=('NonSystemD', '42', '42')): From c2a5fb7f21c04af7a3664e14b4d7e73af23d6db9 Mon Sep 17 00:00:00 2001 From: Joona Hoikkala Date: Sat, 16 Apr 2016 16:03:46 +0300 Subject: [PATCH 027/396] Re-add test file --- certbot/tests/testdata/os-release | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 certbot/tests/testdata/os-release diff --git a/certbot/tests/testdata/os-release b/certbot/tests/testdata/os-release new file mode 100644 index 000000000..b7c3ceb1b --- /dev/null +++ b/certbot/tests/testdata/os-release @@ -0,0 +1,8 @@ +NAME="SystemdOS" +VERSION="42.42.42 LTS, Unreal" +ID=systemdos +ID_LIKE=debian +PRETTY_NAME="SystemdOS 42.42.42 Unreal" +VERSION_ID="42" +HOME_URL="http://www.example.com/" +SUPPORT_URL="http://help.example.com/" From 2646ad4262630d7e05c2ee09b3c91c77d7c48870 Mon Sep 17 00:00:00 2001 From: Noah Swartz Date: Mon, 18 Apr 2016 15:08:16 -0700 Subject: [PATCH 028/396] edit contributing.rst --- docs/contributing.rst | 432 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 432 insertions(+) create mode 100644 docs/contributing.rst diff --git a/docs/contributing.rst b/docs/contributing.rst new file mode 100644 index 000000000..3225de694 --- /dev/null +++ b/docs/contributing.rst @@ -0,0 +1,432 @@ +=============== +Developer Guide +=============== + +.. contents:: Table of Contents + :local: + + +.. _hacking: + +Hacking +======= + +Running a local copy of the client +---------------------------------- + +Running the client in developer mode from your local tree is a little +different than running ``letsencrypt-auto``. To get set up, do these things +once: + +.. code-block:: shell + + git clone https://github.com/letsencrypt/letsencrypt + cd letsencrypt + ./letsencrypt-auto-source/letsencrypt-auto --os-packages-only + ./tools/venv.sh + +Then in each shell where you're working on the client, do: + +.. code-block:: shell + + source ./venv/bin/activate + +After that, your shell will be using the virtual environment, and you run the +client by typing: + +.. code-block:: shell + + certbot + +Activating a shell in this way makes it easier to run unit tests +with ``tox`` and integration tests, as described below. To reverse this, you +can type ``deactivate``. More information can be found in the `virtualenv docs`_. + +.. _`virtualenv docs`: https://virtualenv.pypa.io + +Find issues to work on +---------------------- + +You can find the open issues in the `github issue tracker`_. Comparatively +easy ones are marked `Good Volunteer Task`_. If you're starting work on +something, post a comment to let others know and seek feedback on your plan +where appropriate. + +Once you've got a working branch, you can open a pull request. All changes in +your pull request must have thorough unit test coverage, pass our +`integration`_ tests, and be compliant with the :ref:`coding style +`. + +.. _github issue tracker: https://github.com/certbot/certbot/issues +.. _Good Volunteer Task: https://github.com/certbot/certbot/issues?q=is%3Aopen+is%3Aissue+label%3A%22Good+Volunteer+Task%22 + +Testing +------- + +The following tools are there to help you: + +- ``tox`` starts a full set of tests. Please note that it includes + apacheconftest, which uses the system's Apache install to test config file + parsing, so it should only be run on systems that have an + experimental, non-production Apache2 install on them. ``tox -e + apacheconftest`` can be used to run those specific Apache conf tests. + +- ``tox -e py27``, ``tox -e py26`` etc, run unit tests for specific Python + versions. + +- ``tox -e cover`` checks the test coverage only. Calling the + ``./tox.cover.sh`` script directly (or even ``./tox.cover.sh $pkg1 + $pkg2 ...`` for any subpackages) might be a bit quicker, though. + +- ``tox -e lint`` checks the style of the whole project, while + ``pylint --rcfile=.pylintrc path`` will check a single file or + specific directory only. + +- For debugging, we recommend ``pip install ipdb`` and putting + ``import ipdb; ipdb.set_trace()`` statement inside the source + code. Alternatively, you can use Python's standard library `pdb`, + but you won't get TAB completion... + + +.. _integration: + +Integration testing with the boulder CA +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Generally it is sufficient to open a pull request and let Github and Travis run +integration tests for you. + +However, if you prefer to run tests, you can use Vagrant, using the Vagrantfile +in Certbot's repository. To execute the tests on a Vagrant box, the only +command you are required to run is:: + + ./tests/boulder-integration.sh + +Otherwise, please follow the following instructions. + +Mac OS X users: Run ``./tests/mac-bootstrap.sh`` instead of +``boulder-start.sh`` to install dependencies, configure the +environment, and start boulder. + +Otherwise, install `Go`_ 1.5, ``libtool-ltdl``, ``mariadb-server`` and +``rabbitmq-server`` and then start Boulder_, an ACME CA server. + +If you can't get packages of Go 1.5 for your Linux system, +you can execute the following commands to install it: + +.. code-block:: shell + + wget https://storage.googleapis.com/golang/go1.5.3.linux-amd64.tar.gz -P /tmp/ + sudo tar -C /usr/local -xzf /tmp/go1.5.3.linux-amd64.tar.gz + if ! grep -Fxq "export GOROOT=/usr/local/go" ~/.profile ; then echo "export GOROOT=/usr/local/go" >> ~/.profile; fi + if ! grep -Fxq "export PATH=\\$GOROOT/bin:\\$PATH" ~/.profile ; then echo "export PATH=\\$GOROOT/bin:\\$PATH" >> ~/.profile; fi + +These commands download `Go`_ 1.5.3 to ``/tmp/``, extracts to ``/usr/local``, +and then adds the export lines required to execute ``boulder-start.sh`` to +``~/.profile`` if they were not previously added + +Make sure you execute the following command after `Go`_ finishes installing:: + + if ! grep -Fxq "export GOPATH=\\$HOME/go" ~/.profile ; then echo "export GOPATH=\\$HOME/go" >> ~/.profile; fi + +Afterwards, you'd be able to start Boulder_ using the following command:: + + ./tests/boulder-start.sh + +The script will download, compile and run the executable; please be +patient - it will take some time... Once its ready, you will see +``Server running, listening on 127.0.0.1:4000...``. Add ``/etc/hosts`` +entries pointing ``le.wtf``, ``le1.wtf``, ``le2.wtf``, ``le3.wtf`` +and ``nginx.wtf`` to 127.0.0.1. You may now run (in a separate terminal):: + + ./tests/boulder-integration.sh && echo OK || echo FAIL + +If you would like to test `certbot_nginx` plugin (highly +encouraged) make sure to install prerequisites as listed in +``certbot-nginx/tests/boulder-integration.sh`` and rerun +the integration tests suite. + +.. _Boulder: https://github.com/certbot/boulder +.. _Go: https://golang.org + + +Code components and layout +========================== + +acme + contains all protocol specific code +certbot + all client code + + +Plugin-architecture +------------------- + +Certbot has a plugin architecture to facilitate support for +different webservers, other TLS servers, and operating systems. +The interfaces available for plugins to implement are defined in +`interfaces.py`_ and `plugins/common.py`_. + +The most common kind of plugin is a "Configurator", which is likely to +implement the `~certbot.interfaces.IAuthenticator` and +`~certbot.interfaces.IInstaller` interfaces (though some +Configurators may implement just one of those). + +There are also `~certbot.interfaces.IDisplay` plugins, +which implement bindings to alternative UI libraries. + +.. _interfaces.py: https://github.com/certbot/certbot/blob/master/certbot/interfaces.py +.. _plugins/common.py: https://github.com/certbot/certbot/blob/master/certbot/plugins/common.py#L34 + + +Authenticators +-------------- + +Authenticators are plugins designed to prove that this client deserves a +certificate for some domain name by solving challenges received from +the ACME server. From the protocol, there are essentially two +different types of challenges. Challenges that must be solved by +individual plugins in order to satisfy domain validation (subclasses +of `~.DVChallenge`, i.e. `~.challenges.TLSSNI01`, +`~.challenges.HTTP01`, `~.challenges.DNS`) and continuity specific +challenges (subclasses of `~.ContinuityChallenge`, +i.e. `~.challenges.RecoveryToken`, `~.challenges.RecoveryContact`, +`~.challenges.ProofOfPossession`). Continuity challenges are +always handled by the `~.ContinuityAuthenticator`, while plugins are +expected to handle `~.DVChallenge` types. +Right now, we have two authenticator plugins, the `~.ApacheConfigurator` +and the `~.StandaloneAuthenticator`. The Standalone and Apache +authenticators only solve the `~.challenges.TLSSNI01` challenge currently. +(You can set which challenges your authenticator can handle through the +:meth:`~.IAuthenticator.get_chall_pref`. + +(FYI: We also have a partial implementation for a `~.DNSAuthenticator` +in a separate branch). + + +Installer +--------- + +Installers plugins exist to actually setup the certificate in a server, +possibly tweak the security configuration to make it more correct and secure +(Fix some mixed content problems, turn on HSTS, redirect to HTTPS, etc). +Installer plugins tell the main client about their abilities to do the latter +via the :meth:`~.IInstaller.supported_enhancements` call. We currently +have two Installers in the tree, the `~.ApacheConfigurator`. and the +`~.NginxConfigurator`. External projects have made some progress toward +support for IIS, Icecast and Plesk. + +Installers and Authenticators will oftentimes be the same class/object +(because for instance both tasks can be performed by a webserver like nginx) +though this is not always the case (the standalone plugin is an authenticator +that listens on port 443, but it cannot install certs; a postfix plugin would +be an installer but not an authenticator). + +Installers and Authenticators are kept separate because +it should be possible to use the `~.StandaloneAuthenticator` (it sets +up its own Python server to perform challenges) with a program that +cannot solve challenges itself (Such as MTA installers). + + +Installer Development +--------------------- + +There are a few existing classes that may be beneficial while +developing a new `~certbot.interfaces.IInstaller`. +Installers aimed to reconfigure UNIX servers may use Augeas for +configuration parsing and can inherit from `~.AugeasConfigurator` class +to handle much of the interface. Installers that are unable to use +Augeas may still find the `~.Reverter` class helpful in handling +configuration checkpoints and rollback. + + +Display +~~~~~~~ + +We currently offer a pythondialog and "text" mode for displays. Display +plugins implement the `~certbot.interfaces.IDisplay` +interface. + +.. _dev-plugin: + +Writing your own plugin +======================= + +Certbot client supports dynamic discovery of plugins through the +`setuptools entry points`_. This way you can, for example, create a +custom implementation of `~certbot.interfaces.IAuthenticator` or +the `~certbot.interfaces.IInstaller` without having to merge it +with the core upstream source code. An example is provided in +``examples/plugins/`` directory. + +.. warning:: Please be aware though that as this client is still in a + developer-preview stage, the API may undergo a few changes. If you + believe the plugin will be beneficial to the community, please + consider submitting a pull request to the repo and we will update + it with any necessary API changes. + +.. _`setuptools entry points`: + https://pythonhosted.org/setuptools/setuptools.html#dynamic-discovery-of-services-and-plugins + + +.. _coding-style: + +Coding style +============ + +Please: + +1. **Be consistent with the rest of the code**. + +2. Read `PEP 8 - Style Guide for Python Code`_. + +3. Follow the `Google Python Style Guide`_, with the exception that we + use `Sphinx-style`_ documentation:: + + def foo(arg): + """Short description. + + :param int arg: Some number. + + :returns: Argument + :rtype: int + + """ + return arg + +4. Remember to use ``pylint``. + +.. _Google Python Style Guide: + https://google-styleguide.googlecode.com/svn/trunk/pyguide.html +.. _Sphinx-style: http://sphinx-doc.org/ +.. _PEP 8 - Style Guide for Python Code: + https://www.python.org/dev/peps/pep-0008 + +Submitting a pull request +========================= + +Steps: + +1. Write your code! +2. Make sure your environment is set up properly and that you're in your + virtualenv. You can do this by running ``./tools/venv.sh``. + (this is a **very important** step) +3. Run ``./pep8.travis.sh`` to do a cursory check of your code style. + Fix any errors. +4. Run ``tox -e lint`` to check for pylint errors. Fix any errors. +5. Run ``tox`` to run the entire test suite including coverage. Fix any errors. +6. If your code touches communication with an ACME server/Boulder, you + should run the integration tests, see `integration`_. See `Known Issues`_ + for some common failures that have nothing to do with your code. +7. Submit the PR. +8. Did your tests pass on Travis? If they didn't, it might not be your fault! + See `Known Issues`_. If it's not a known issue, fix any errors. + +.. _Known Issues: + https://github.com/certbot/certbot/wiki/Known-issues + +Updating the documentation +========================== + +In order to generate the Sphinx documentation, run the following +commands: + +.. code-block:: shell + + make -C docs clean html + +This should generate documentation in the ``docs/_build/html`` +directory. + + +Other methods for running the client +==================================== + +Vagrant +------- + +If you are a Vagrant user, Certbot comes with a Vagrantfile that +automates setting up a development environment in an Ubuntu 14.04 +LTS VM. To set it up, simply run ``vagrant up``. The repository is +synced to ``/vagrant``, so you can get started with: + +.. code-block:: shell + + vagrant ssh + cd /vagrant + sudo ./venv/bin/certbot + +Support for other Linux distributions coming soon. + +.. note:: + Unfortunately, Python distutils and, by extension, setup.py and + tox, use hard linking quite extensively. Hard linking is not + supported by the default sync filesystem in Vagrant. As a result, + all actions with these commands are *significantly slower* in + Vagrant. One potential fix is to `use NFS`_ (`related issue`_). + +.. _use NFS: http://docs.vagrantup.com/v2/synced-folders/nfs.html +.. _related issue: https://github.com/ClusterHQ/flocker/issues/516 + + +Docker +------ + +OSX users will probably find it easiest to set up a Docker container for +development. Certbot comes with a Dockerfile (``Dockerfile-dev``) +for doing so. To use Docker on OSX, install and setup docker-machine using the +instructions at https://docs.docker.com/installation/mac/. + +To build the development Docker image:: + + docker build -t certbot -f Dockerfile-dev . + +Now run tests inside the Docker image: + +.. code-block:: shell + + docker run -it certbot bash + cd src + tox -e py27 + + +.. _prerequisites: + +Notes on OS dependencies +======================== + +OS-level dependencies can be installed like so: + +.. code-block:: shell + + letsencrypt-auto-source/letsencrypt-auto --os-packages-only + +In general... + +* ``sudo`` is required as a suggested way of running privileged process +* `Python`_ 2.6/2.7 is required +* `Augeas`_ is required for the Python bindings +* ``virtualenv`` and ``pip`` are used for managing other python library + dependencies + +.. _Python: https://wiki.python.org/moin/BeginnersGuide/Download +.. _Augeas: http://augeas.net/ +.. _Virtualenv: https://virtualenv.pypa.io + + +Debian +------ + +For squeeze you will need to: + +- Use ``virtualenv --no-site-packages -p python`` instead of ``-p python2``. + + +FreeBSD +------- + +Package installation for FreeBSD uses ``pkg``, not ports. + +FreeBSD by default uses ``tcsh``. In order to activate virtualenv (see +below), you will need a compatible shell, e.g. ``pkg install bash && +bash``. From 5b597e0e8b28b52774bd94fc55b9ff09a1994d55 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Wed, 20 Apr 2016 09:28:11 +1000 Subject: [PATCH 029/396] Rebuild letsencrypt-auto from current source --- letsencrypt-auto-source/letsencrypt-auto | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/letsencrypt-auto-source/letsencrypt-auto b/letsencrypt-auto-source/letsencrypt-auto index 111f2b272..1cd04e506 100755 --- a/letsencrypt-auto-source/letsencrypt-auto +++ b/letsencrypt-auto-source/letsencrypt-auto @@ -1,6 +1,6 @@ #!/bin/sh # -# Download and run the latest release version of the Let's Encrypt client. +# Download and run the latest release version of the Certbot client. # # NOTE: THIS SCRIPT IS AUTO-GENERATED AND SELF-UPDATING # @@ -46,7 +46,7 @@ for arg in "$@" ; do done # letsencrypt-auto needs root access to bootstrap OS dependencies, and -# letsencrypt itself needs root access for almost all modes of operation +# certbot itself needs root access for almost all modes of operation # The "normal" case is that sudo is used for the steps that need root, but # this script *can* be run as root (not recommended), or fall back to using # `su` @@ -186,7 +186,7 @@ BootstrapDebCommon() { AddBackportRepo precise-backports "deb http://archive.ubuntu.com/ubuntu precise-backports main restricted universe multiverse" else echo "No libaugeas0 version is available that's new enough to run the" - echo "Let's Encrypt apache plugin..." + echo "Certbot apache plugin..." fi # XXX add a case for ubuntu PPAs fi @@ -426,7 +426,7 @@ Bootstrap() { elif grep -iq "Amazon Linux" /etc/issue ; then ExperimentalBootstrap "Amazon Linux" BootstrapRpmCommon else - echo "Sorry, I don't know how to bootstrap Let's Encrypt on your operating system!" + echo "Sorry, I don't know how to bootstrap Certbot on your operating system!" echo echo "You will need to bootstrap, configure virtualenv, and run pip install manually." echo "Please see https://letsencrypt.readthedocs.org/en/latest/contributing.html#prerequisites" @@ -466,7 +466,7 @@ if [ "$1" = "--le-auto-phase2" ]; then # ------------------------------------------------------------------------- cat << "UNLIKELY_EOF" > "$TEMP_DIR/letsencrypt-auto-requirements.txt" # This is the flattened list of packages letsencrypt-auto installs. To generate -# this, do `pip install --no-cache-dir -e acme -e . -e letsencrypt-apache`, and +# this, do `pip install --no-cache-dir -e acme -e . -e certbot-apache`, and # then use `hashin` or a more secure method to gather the hashes. argparse==1.4.0 \ @@ -823,7 +823,7 @@ UNLIKELY_EOF fi echo "Installation succeeded." fi - echo "Requesting root privileges to run letsencrypt..." + echo "Requesting root privileges to run certbot..." echo " " $SUDO "$VENV_BIN/letsencrypt" "$@" $SUDO "$VENV_BIN/letsencrypt" "$@" else @@ -831,8 +831,8 @@ else # # Each phase checks the version of only the thing it is responsible for # upgrading. Phase 1 checks the version of the latest release of - # letsencrypt-auto (which is always the same as that of the letsencrypt - # package). Phase 2 checks the version of the locally installed letsencrypt. + # letsencrypt-auto (which is always the same as that of the certbot + # package). Phase 2 checks the version of the locally installed certbot. if [ ! -f "$VENV_BIN/letsencrypt" ]; then # If it looks like we've never bootstrapped before, bootstrap: From b597f4a284ac17a20482808b266ece29b6fecb99 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Wed, 20 Apr 2016 09:28:41 +1000 Subject: [PATCH 030/396] [letsencrypt-auto] handle network/pypi failures more gracefully --- letsencrypt-auto-source/letsencrypt-auto | 5 +++-- letsencrypt-auto-source/letsencrypt-auto.template | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/letsencrypt-auto-source/letsencrypt-auto b/letsencrypt-auto-source/letsencrypt-auto index 1cd04e506..81b1801cf 100755 --- a/letsencrypt-auto-source/letsencrypt-auto +++ b/letsencrypt-auto-source/letsencrypt-auto @@ -978,8 +978,9 @@ if __name__ == '__main__': UNLIKELY_EOF # --------------------------------------------------------------------------- DeterminePythonVersion - REMOTE_VERSION=`"$LE_PYTHON" "$TEMP_DIR/fetch.py" --latest-version` - if [ "$LE_AUTO_VERSION" != "$REMOTE_VERSION" ]; then + if ! REMOTE_VERSION=`"$LE_PYTHON" "$TEMP_DIR/fetch.py" --latest-version` ; then + echo "WARNING: unable to check for updates." + elif [ "$LE_AUTO_VERSION" != "$REMOTE_VERSION" ]; then echo "Upgrading letsencrypt-auto $LE_AUTO_VERSION to $REMOTE_VERSION..." # Now we drop into Python so we don't have to install even more diff --git a/letsencrypt-auto-source/letsencrypt-auto.template b/letsencrypt-auto-source/letsencrypt-auto.template index 2c8e1ec4c..168dea2af 100755 --- a/letsencrypt-auto-source/letsencrypt-auto.template +++ b/letsencrypt-auto-source/letsencrypt-auto.template @@ -248,8 +248,9 @@ else UNLIKELY_EOF # --------------------------------------------------------------------------- DeterminePythonVersion - REMOTE_VERSION=`"$LE_PYTHON" "$TEMP_DIR/fetch.py" --latest-version` - if [ "$LE_AUTO_VERSION" != "$REMOTE_VERSION" ]; then + if ! REMOTE_VERSION=`"$LE_PYTHON" "$TEMP_DIR/fetch.py" --latest-version` ; then + echo "WARNING: unable to check for updates." + elif [ "$LE_AUTO_VERSION" != "$REMOTE_VERSION" ]; then echo "Upgrading letsencrypt-auto $LE_AUTO_VERSION to $REMOTE_VERSION..." # Now we drop into Python so we don't have to install even more From 3c455b7e64f44deb8a47f77f8c7e70b67db3aab9 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Wed, 20 Apr 2016 10:45:30 +1000 Subject: [PATCH 031/396] letsencrypt-auto: set CERTBOT_AUTO :) --- letsencrypt-auto-source/letsencrypt-auto | 7 +++++-- letsencrypt-auto-source/letsencrypt-auto.template | 7 +++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/letsencrypt-auto-source/letsencrypt-auto b/letsencrypt-auto-source/letsencrypt-auto index 81b1801cf..1f2dfcf85 100755 --- a/letsencrypt-auto-source/letsencrypt-auto +++ b/letsencrypt-auto-source/letsencrypt-auto @@ -50,9 +50,12 @@ done # The "normal" case is that sudo is used for the steps that need root, but # this script *can* be run as root (not recommended), or fall back to using # `su` +SUDO_ENV="" +export CERTBOT_AUTO="$0" if test "`id -u`" -ne "0" ; then if command -v sudo 1>/dev/null 2>&1; then SUDO=sudo + SUDO_ENV="CERTBOT_AUTO=\"$0\"" else echo \"sudo\" is not available, will use \"su\" for installation steps... # Because the parameters in `su -c` has to be a string, @@ -824,8 +827,8 @@ UNLIKELY_EOF echo "Installation succeeded." fi echo "Requesting root privileges to run certbot..." - echo " " $SUDO "$VENV_BIN/letsencrypt" "$@" - $SUDO "$VENV_BIN/letsencrypt" "$@" + echo " " $SUDO $SUDO_ENV "$VENV_BIN/letsencrypt" "$@" + $SUDO $SUDO_ENV "$VENV_BIN/letsencrypt" "$@" else # Phase 1: Upgrade letsencrypt-auto if neceesary, then self-invoke. # diff --git a/letsencrypt-auto-source/letsencrypt-auto.template b/letsencrypt-auto-source/letsencrypt-auto.template index 168dea2af..580ee7c07 100755 --- a/letsencrypt-auto-source/letsencrypt-auto.template +++ b/letsencrypt-auto-source/letsencrypt-auto.template @@ -50,9 +50,12 @@ done # The "normal" case is that sudo is used for the steps that need root, but # this script *can* be run as root (not recommended), or fall back to using # `su` +SUDO_ENV="" +export CERTBOT_AUTO="$0" if test "`id -u`" -ne "0" ; then if command -v sudo 1>/dev/null 2>&1; then SUDO=sudo + SUDO_ENV="CERTBOT_AUTO=\"$0\"" else echo \"sudo\" is not available, will use \"su\" for installation steps... # Because the parameters in `su -c` has to be a string, @@ -220,8 +223,8 @@ UNLIKELY_EOF echo "Installation succeeded." fi echo "Requesting root privileges to run certbot..." - echo " " $SUDO "$VENV_BIN/letsencrypt" "$@" - $SUDO "$VENV_BIN/letsencrypt" "$@" + echo " " $SUDO $SUDO_ENV "$VENV_BIN/letsencrypt" "$@" + $SUDO $SUDO_ENV "$VENV_BIN/letsencrypt" "$@" else # Phase 1: Upgrade letsencrypt-auto if neceesary, then self-invoke. # From a73986576318d9fdbe95621b37caa72bbb470797 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Wed, 20 Apr 2016 10:49:02 +1000 Subject: [PATCH 032/396] Print a deprecation warning with the ancient letsencrypt-auto --- certbot/cli.py | 19 ++++++++++++++++++- certbot/main.py | 1 + 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/certbot/cli.py b/certbot/cli.py index e2c57595b..3e6f78bc1 100644 --- a/certbot/cli.py +++ b/certbot/cli.py @@ -37,8 +37,9 @@ helpful_parser = None # should only be used for purposes where inability to detect letsencrypt-auto # fails safely +LEAUTO = "letsencrypt-auto" fragment = os.path.join(".local", "share", "certbot") -cli_command = "letsencrypt-auto" if fragment in sys.argv[0] else "certbot" +cli_command = LEAUTO if fragment in sys.argv[0] else "certbot" # Argparse's help formatting has a lot of unhelpful peculiarities, so we want # to replace as much of it as we can... @@ -141,6 +142,22 @@ def usage_strings(plugins): return USAGE % (apache_doc, nginx_doc), SHORT_USAGE +def possible_deprecation_warning(config): + "A deprecation warning for users with the old, not-self-upgrading letsencrypt-auto." + if cli_command != LEAUTO: + return + if config.no_self_upgrade: + # users setting --no-self-upgrade might be hanging on a clent version like 0.3.0 + # or 0.5.0 which is the new script, but doesn't set CERTBOT_AUTO; they don't + # need warnings + return + if "CERTBOT_AUTO" not in os.environ: + logger.warn("You are running with an old copy of letsencrypt-auto that does " + "not receive updates, and is less reliable than more recent versions. " + "We recommend upgrading to the latest certbot-auto script, or using native " + "OS packages.") + + class _Default(object): """A class to use as a default to detect if a value is set by a user""" diff --git a/certbot/main.py b/certbot/main.py index 72f4fe66e..05347734e 100644 --- a/certbot/main.py +++ b/certbot/main.py @@ -649,6 +649,7 @@ def main(cli_args=sys.argv[1:]): args = cli.prepare_and_parse_args(plugins, cli_args) config = configuration.NamespaceConfig(args) zope.component.provideUtility(config) + cli.possible_deprecation_warning(config) # Setup logging ASAP, otherwise "No handlers could be found for # logger ..." TODO: this should be done before plugins discovery From 45681909c7fba2e0b061f6e5abe471d252c45a08 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 20 Apr 2016 14:39:26 -0400 Subject: [PATCH 033/396] Selectively rename le-auto strings --- letsencrypt-auto-source/letsencrypt-auto | 38 +++++++++---------- .../letsencrypt-auto.template | 20 +++++----- letsencrypt-auto-source/pieces/fetch.py | 2 +- .../pieces/letsencrypt-auto-requirements.txt | 2 +- letsencrypt-auto-source/tests/auto_test.py | 12 +++--- 5 files changed, 35 insertions(+), 39 deletions(-) diff --git a/letsencrypt-auto-source/letsencrypt-auto b/letsencrypt-auto-source/letsencrypt-auto index 111f2b272..c137c4978 100755 --- a/letsencrypt-auto-source/letsencrypt-auto +++ b/letsencrypt-auto-source/letsencrypt-auto @@ -1,6 +1,6 @@ #!/bin/sh # -# Download and run the latest release version of the Let's Encrypt client. +# Download and run the latest release version of the Certbot client. # # NOTE: THIS SCRIPT IS AUTO-GENERATED AND SELF-UPDATING # @@ -21,7 +21,7 @@ VENV_PATH=${VENV_PATH:-"$XDG_DATA_HOME/$VENV_NAME"} VENV_BIN="$VENV_PATH/bin" LE_AUTO_VERSION="0.6.0.dev0" -# This script takes the same arguments as the main letsencrypt program, but it +# This script takes the same arguments as the main certbot program, but it # additionally responds to --verbose (more output) and --debug (allow support # for experimental platforms) for arg in "$@" ; do @@ -45,8 +45,8 @@ for arg in "$@" ; do esac done -# letsencrypt-auto needs root access to bootstrap OS dependencies, and -# letsencrypt itself needs root access for almost all modes of operation +# certbot-auto needs root access to bootstrap OS dependencies, and +# certbot itself needs root access for almost all modes of operation # The "normal" case is that sudo is used for the steps that need root, but # this script *can* be run as root (not recommended), or fall back to using # `su` @@ -186,7 +186,7 @@ BootstrapDebCommon() { AddBackportRepo precise-backports "deb http://archive.ubuntu.com/ubuntu precise-backports main restricted universe multiverse" else echo "No libaugeas0 version is available that's new enough to run the" - echo "Let's Encrypt apache plugin..." + echo "Certbot apache plugin..." fi # XXX add a case for ubuntu PPAs fi @@ -426,7 +426,7 @@ Bootstrap() { elif grep -iq "Amazon Linux" /etc/issue ; then ExperimentalBootstrap "Amazon Linux" BootstrapRpmCommon else - echo "Sorry, I don't know how to bootstrap Let's Encrypt on your operating system!" + echo "Sorry, I don't know how to bootstrap Certbot on your operating system!" echo echo "You will need to bootstrap, configure virtualenv, and run pip install manually." echo "Please see https://letsencrypt.readthedocs.org/en/latest/contributing.html#prerequisites" @@ -446,7 +446,8 @@ if [ "$1" = "--le-auto-phase2" ]; then shift 1 # the --le-auto-phase2 arg if [ -f "$VENV_BIN/letsencrypt" ]; then # --version output ran through grep due to python-cryptography DeprecationWarnings - INSTALLED_VERSION=$("$VENV_BIN/letsencrypt" --version 2>&1 | grep ^letsencrypt | cut -d " " -f 2) + # grep for both certbot and letsencrypt until certbot and shim packages have been released + INSTALLED_VERSION=$("$VENV_BIN/letsencrypt" --version 2>&1 | grep "^certbot\|^letsencrypt" | cut -d " " -f 2) else INSTALLED_VERSION="none" fi @@ -465,8 +466,8 @@ if [ "$1" = "--le-auto-phase2" ]; then # There is no $ interpolation due to quotes on starting heredoc delimiter. # ------------------------------------------------------------------------- cat << "UNLIKELY_EOF" > "$TEMP_DIR/letsencrypt-auto-requirements.txt" -# This is the flattened list of packages letsencrypt-auto installs. To generate -# this, do `pip install --no-cache-dir -e acme -e . -e letsencrypt-apache`, and +# This is the flattened list of packages certbot-auto installs. To generate +# this, do `pip install --no-cache-dir -e acme -e . -e certbot-apache`, and # then use `hashin` or a more secure method to gather the hashes. argparse==1.4.0 \ @@ -823,16 +824,16 @@ UNLIKELY_EOF fi echo "Installation succeeded." fi - echo "Requesting root privileges to run letsencrypt..." + echo "Requesting root privileges to run certbot..." echo " " $SUDO "$VENV_BIN/letsencrypt" "$@" $SUDO "$VENV_BIN/letsencrypt" "$@" else - # Phase 1: Upgrade letsencrypt-auto if neceesary, then self-invoke. + # Phase 1: Upgrade certbot-auto if neceesary, then self-invoke. # # Each phase checks the version of only the thing it is responsible for # upgrading. Phase 1 checks the version of the latest release of - # letsencrypt-auto (which is always the same as that of the letsencrypt - # package). Phase 2 checks the version of the locally installed letsencrypt. + # certbot-auto (which is always the same as that of the certbot + # package). Phase 2 checks the version of the locally installed certbot. if [ ! -f "$VENV_BIN/letsencrypt" ]; then # If it looks like we've never bootstrapped before, bootstrap: @@ -953,7 +954,7 @@ def verified_new_le_auto(get, tag, temp_dir): stderr=dev_null) except CalledProcessError as exc: raise ExpectedError("Couldn't verify signature of downloaded " - "letsencrypt-auto.", exc) + "certbot-auto.", exc) def main(): @@ -980,27 +981,24 @@ UNLIKELY_EOF DeterminePythonVersion REMOTE_VERSION=`"$LE_PYTHON" "$TEMP_DIR/fetch.py" --latest-version` if [ "$LE_AUTO_VERSION" != "$REMOTE_VERSION" ]; then - echo "Upgrading letsencrypt-auto $LE_AUTO_VERSION to $REMOTE_VERSION..." + echo "Upgrading certbot-auto $LE_AUTO_VERSION to $REMOTE_VERSION..." # Now we drop into Python so we don't have to install even more # dependencies (curl, etc.), for better flow control, and for the option of # future Windows compatibility. "$LE_PYTHON" "$TEMP_DIR/fetch.py" --le-auto-script "v$REMOTE_VERSION" - # Install new copy of letsencrypt-auto. + # Install new copy of certbot-auto. # TODO: Deal with quotes in pathnames. - echo "Replacing letsencrypt-auto..." + echo "Replacing certbot-auto..." # Clone permissions with cp. chmod and chown don't have a --reference # option on OS X or BSD, and stat -c on Linux is stat -f on OS X and BSD: - echo " " $SUDO cp -p "$0" "$TEMP_DIR/letsencrypt-auto.permission-clone" $SUDO cp -p "$0" "$TEMP_DIR/letsencrypt-auto.permission-clone" - echo " " $SUDO cp "$TEMP_DIR/letsencrypt-auto" "$TEMP_DIR/letsencrypt-auto.permission-clone" $SUDO cp "$TEMP_DIR/letsencrypt-auto" "$TEMP_DIR/letsencrypt-auto.permission-clone" # Using mv rather than cp leaves the old file descriptor pointing to the # original copy so the shell can continue to read it unmolested. mv across # filesystems is non-atomic, doing `rm dest, cp src dest, rm src`, but the # cp is unlikely to fail (esp. under sudo) if the rm doesn't. - echo " " $SUDO mv -f "$TEMP_DIR/letsencrypt-auto.permission-clone" "$0" $SUDO mv -f "$TEMP_DIR/letsencrypt-auto.permission-clone" "$0" # TODO: Clean up temp dir safely, even if it has quotes in its path. rm -rf "$TEMP_DIR" diff --git a/letsencrypt-auto-source/letsencrypt-auto.template b/letsencrypt-auto-source/letsencrypt-auto.template index 2c8e1ec4c..67f82febf 100755 --- a/letsencrypt-auto-source/letsencrypt-auto.template +++ b/letsencrypt-auto-source/letsencrypt-auto.template @@ -21,7 +21,7 @@ VENV_PATH=${VENV_PATH:-"$XDG_DATA_HOME/$VENV_NAME"} VENV_BIN="$VENV_PATH/bin" LE_AUTO_VERSION="{{ LE_AUTO_VERSION }}" -# This script takes the same arguments as the main letsencrypt program, but it +# This script takes the same arguments as the main certbot program, but it # additionally responds to --verbose (more output) and --debug (allow support # for experimental platforms) for arg in "$@" ; do @@ -45,7 +45,7 @@ for arg in "$@" ; do esac done -# letsencrypt-auto needs root access to bootstrap OS dependencies, and +# certbot-auto needs root access to bootstrap OS dependencies, and # certbot itself needs root access for almost all modes of operation # The "normal" case is that sudo is used for the steps that need root, but # this script *can* be run as root (not recommended), or fall back to using @@ -177,7 +177,8 @@ if [ "$1" = "--le-auto-phase2" ]; then shift 1 # the --le-auto-phase2 arg if [ -f "$VENV_BIN/letsencrypt" ]; then # --version output ran through grep due to python-cryptography DeprecationWarnings - INSTALLED_VERSION=$("$VENV_BIN/letsencrypt" --version 2>&1 | grep ^letsencrypt | cut -d " " -f 2) + # grep for both certbot and letsencrypt until certbot and shim packages have been released + INSTALLED_VERSION=$("$VENV_BIN/letsencrypt" --version 2>&1 | grep "^certbot\|^letsencrypt" | cut -d " " -f 2) else INSTALLED_VERSION="none" fi @@ -223,11 +224,11 @@ UNLIKELY_EOF echo " " $SUDO "$VENV_BIN/letsencrypt" "$@" $SUDO "$VENV_BIN/letsencrypt" "$@" else - # Phase 1: Upgrade letsencrypt-auto if neceesary, then self-invoke. + # Phase 1: Upgrade certbot-auto if neceesary, then self-invoke. # # Each phase checks the version of only the thing it is responsible for # upgrading. Phase 1 checks the version of the latest release of - # letsencrypt-auto (which is always the same as that of the certbot + # certbot-auto (which is always the same as that of the certbot # package). Phase 2 checks the version of the locally installed certbot. if [ ! -f "$VENV_BIN/letsencrypt" ]; then @@ -250,27 +251,24 @@ UNLIKELY_EOF DeterminePythonVersion REMOTE_VERSION=`"$LE_PYTHON" "$TEMP_DIR/fetch.py" --latest-version` if [ "$LE_AUTO_VERSION" != "$REMOTE_VERSION" ]; then - echo "Upgrading letsencrypt-auto $LE_AUTO_VERSION to $REMOTE_VERSION..." + echo "Upgrading certbot-auto $LE_AUTO_VERSION to $REMOTE_VERSION..." # Now we drop into Python so we don't have to install even more # dependencies (curl, etc.), for better flow control, and for the option of # future Windows compatibility. "$LE_PYTHON" "$TEMP_DIR/fetch.py" --le-auto-script "v$REMOTE_VERSION" - # Install new copy of letsencrypt-auto. + # Install new copy of certbot-auto. # TODO: Deal with quotes in pathnames. - echo "Replacing letsencrypt-auto..." + echo "Replacing certbot-auto..." # Clone permissions with cp. chmod and chown don't have a --reference # option on OS X or BSD, and stat -c on Linux is stat -f on OS X and BSD: - echo " " $SUDO cp -p "$0" "$TEMP_DIR/letsencrypt-auto.permission-clone" $SUDO cp -p "$0" "$TEMP_DIR/letsencrypt-auto.permission-clone" - echo " " $SUDO cp "$TEMP_DIR/letsencrypt-auto" "$TEMP_DIR/letsencrypt-auto.permission-clone" $SUDO cp "$TEMP_DIR/letsencrypt-auto" "$TEMP_DIR/letsencrypt-auto.permission-clone" # Using mv rather than cp leaves the old file descriptor pointing to the # original copy so the shell can continue to read it unmolested. mv across # filesystems is non-atomic, doing `rm dest, cp src dest, rm src`, but the # cp is unlikely to fail (esp. under sudo) if the rm doesn't. - echo " " $SUDO mv -f "$TEMP_DIR/letsencrypt-auto.permission-clone" "$0" $SUDO mv -f "$TEMP_DIR/letsencrypt-auto.permission-clone" "$0" # TODO: Clean up temp dir safely, even if it has quotes in its path. rm -rf "$TEMP_DIR" diff --git a/letsencrypt-auto-source/pieces/fetch.py b/letsencrypt-auto-source/pieces/fetch.py index 39ff7777c..38f4aa255 100644 --- a/letsencrypt-auto-source/pieces/fetch.py +++ b/letsencrypt-auto-source/pieces/fetch.py @@ -103,7 +103,7 @@ def verified_new_le_auto(get, tag, temp_dir): stderr=dev_null) except CalledProcessError as exc: raise ExpectedError("Couldn't verify signature of downloaded " - "letsencrypt-auto.", exc) + "certbot-auto.", exc) def main(): diff --git a/letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt b/letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt index 27cfb3d43..3a1ce34f1 100644 --- a/letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt +++ b/letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt @@ -1,4 +1,4 @@ -# This is the flattened list of packages letsencrypt-auto installs. To generate +# This is the flattened list of packages certbot-auto installs. To generate # this, do `pip install --no-cache-dir -e acme -e . -e certbot-apache`, and # then use `hashin` or a more secure method to gather the hashes. diff --git a/letsencrypt-auto-source/tests/auto_test.py b/letsencrypt-auto-source/tests/auto_test.py index edb5f0c04..3b7e8731b 100644 --- a/letsencrypt-auto-source/tests/auto_test.py +++ b/letsencrypt-auto-source/tests/auto_test.py @@ -231,7 +231,7 @@ class AutoTests(TestCase): * The OpenSSL sig mismatches. For tests which get to the end, we run merely ``letsencrypt --version``. - The functioning of the rest of the letsencrypt script is covered by other + The functioning of the rest of the certbot script is covered by other test suites. """ @@ -277,7 +277,7 @@ class AutoTests(TestCase): ok_(re.match(r'letsencrypt \d+\.\d+\.\d+', err.strip().splitlines()[-1])) # Make a few assertions to test the validity of the next tests: - self.assertIn('Upgrading letsencrypt-auto ', out) + self.assertIn('Upgrading certbot-auto ', out) self.assertIn('Creating virtual environment...', out) # Now we have le-auto 99.9.9 and LE 99.9.9 installed. This @@ -286,14 +286,14 @@ class AutoTests(TestCase): # Test when neither phase-1 upgrade nor phase-2 upgrade is # needed (probably a common case): out, err = run_letsencrypt_auto() - self.assertNotIn('Upgrading letsencrypt-auto ', out) + self.assertNotIn('Upgrading certbot-auto ', out) self.assertNotIn('Creating virtual environment...', out) # Test when a phase-1 upgrade is not needed but a phase-2 # upgrade is: set_le_script_version(venv_dir, '0.0.1') out, err = run_letsencrypt_auto() - self.assertNotIn('Upgrading letsencrypt-auto ', out) + self.assertNotIn('Upgrading certbot-auto ', out) self.assertIn('Creating virtual environment...', out) def test_openssl_failure(self): @@ -312,10 +312,10 @@ class AutoTests(TestCase): except CalledProcessError as exc: eq_(exc.returncode, 1) self.assertIn("Couldn't verify signature of downloaded " - "letsencrypt-auto.", + "certbot-auto.", exc.output) else: - self.fail('Signature check on letsencrypt-auto erroneously passed.') + self.fail('Signature check on certbot-auto erroneously passed.') def test_pip_failure(self): """Make sure pip stops us if there is a hash mismatch.""" From 530033a37dec26e40ba526d58694f61fd52c6066 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Thu, 21 Apr 2016 15:16:39 -0400 Subject: [PATCH 034/396] Add CLI parsing --- letsencrypt-auto-source/letsencrypt-auto | 10 ++++++++++ letsencrypt-auto-source/letsencrypt-auto.template | 10 ++++++++++ 2 files changed, 20 insertions(+) diff --git a/letsencrypt-auto-source/letsencrypt-auto b/letsencrypt-auto-source/letsencrypt-auto index c137c4978..1b88f1cc1 100755 --- a/letsencrypt-auto-source/letsencrypt-auto +++ b/letsencrypt-auto-source/letsencrypt-auto @@ -34,6 +34,10 @@ for arg in "$@" ; do # Do not upgrade this script (also prevents client upgrades, because each # copy of the script pins a hash of the python client) NO_SELF_UPGRADE=1;; + --help) + HELP=1;; + --yes) + ASSUME_YES=1;; --verbose) VERBOSE=1;; [!-]*|-*[!v]*|-) @@ -81,6 +85,12 @@ else SUDO= fi +if [ $(basename $0) = "letsencrypt-auto" ]; then + # letsencrypt-auto does not respect --help or --yes for backwards compatibility + ASSUME_YES=1 + HELP=0 +fi + ExperimentalBootstrap() { # Arguments: Platform name, bootstrap function name if [ "$DEBUG" = 1 ]; then diff --git a/letsencrypt-auto-source/letsencrypt-auto.template b/letsencrypt-auto-source/letsencrypt-auto.template index 67f82febf..f274418f3 100755 --- a/letsencrypt-auto-source/letsencrypt-auto.template +++ b/letsencrypt-auto-source/letsencrypt-auto.template @@ -34,6 +34,10 @@ for arg in "$@" ; do # Do not upgrade this script (also prevents client upgrades, because each # copy of the script pins a hash of the python client) NO_SELF_UPGRADE=1;; + --help) + HELP=1;; + --yes) + ASSUME_YES=1;; --verbose) VERBOSE=1;; [!-]*|-*[!v]*|-) @@ -81,6 +85,12 @@ else SUDO= fi +if [ $(basename $0) = "letsencrypt-auto" ]; then + # letsencrypt-auto does not respect --help or --yes for backwards compatibility + ASSUME_YES=1 + HELP=0 +fi + ExperimentalBootstrap() { # Arguments: Platform name, bootstrap function name if [ "$DEBUG" = 1 ]; then From 0fa18b608150472801ccddaead63f5b3e6a96f12 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Thu, 21 Apr 2016 15:55:28 -0400 Subject: [PATCH 035/396] Add help text --- letsencrypt-auto-source/letsencrypt-auto | 22 +++++++++++++++---- .../letsencrypt-auto.template | 22 +++++++++++++++---- 2 files changed, 36 insertions(+), 8 deletions(-) diff --git a/letsencrypt-auto-source/letsencrypt-auto b/letsencrypt-auto-source/letsencrypt-auto index 1b88f1cc1..df4783a72 100755 --- a/letsencrypt-auto-source/letsencrypt-auto +++ b/letsencrypt-auto-source/letsencrypt-auto @@ -20,10 +20,24 @@ VENV_NAME="letsencrypt" VENV_PATH=${VENV_PATH:-"$XDG_DATA_HOME/$VENV_NAME"} VENV_BIN="$VENV_PATH/bin" LE_AUTO_VERSION="0.6.0.dev0" +BASENAME=$(basename $0) +USAGE="Usage: $BASENAME [OPTION] +A self-updating wrapper script for the Certbot ACME client. When run, updates +to both this script and certbot will be downloaded and installed. After +ensuring you have the latest versions installed, certbot will be invoked with +all arguments you provided. + +Help for certbot itself cannot be provided until it is installed. + + --debug attempt installation on experimental platforms + --help print this help + --no-self-upgrade do not download updates for certbot or certbot-auto + --os-packages-only install OS dependencies and exit + -v, --verbose provide more output + --yes assume yes is the answer to all prompts + +All arguments are accepted and forwarded to the Certbot client when run." -# This script takes the same arguments as the main certbot program, but it -# additionally responds to --verbose (more output) and --debug (allow support -# for experimental platforms) for arg in "$@" ; do case "$arg" in --debug) @@ -85,7 +99,7 @@ else SUDO= fi -if [ $(basename $0) = "letsencrypt-auto" ]; then +if [ $BASENAME = "letsencrypt-auto" ]; then # letsencrypt-auto does not respect --help or --yes for backwards compatibility ASSUME_YES=1 HELP=0 diff --git a/letsencrypt-auto-source/letsencrypt-auto.template b/letsencrypt-auto-source/letsencrypt-auto.template index f274418f3..a17d11fa4 100755 --- a/letsencrypt-auto-source/letsencrypt-auto.template +++ b/letsencrypt-auto-source/letsencrypt-auto.template @@ -20,10 +20,24 @@ VENV_NAME="letsencrypt" VENV_PATH=${VENV_PATH:-"$XDG_DATA_HOME/$VENV_NAME"} VENV_BIN="$VENV_PATH/bin" LE_AUTO_VERSION="{{ LE_AUTO_VERSION }}" +BASENAME=$(basename $0) +USAGE="Usage: $BASENAME [OPTION] +A self-updating wrapper script for the Certbot ACME client. When run, updates +to both this script and certbot will be downloaded and installed. After +ensuring you have the latest versions installed, certbot will be invoked with +all arguments you provided. + +Help for certbot itself cannot be provided until it is installed. + + --debug attempt installation on experimental platforms + --help print this help + --no-self-upgrade do not download updates for certbot or certbot-auto + --os-packages-only install OS dependencies and exit + -v, --verbose provide more output + --yes assume yes is the answer to all prompts + +All arguments are accepted and forwarded to the Certbot client when run." -# This script takes the same arguments as the main certbot program, but it -# additionally responds to --verbose (more output) and --debug (allow support -# for experimental platforms) for arg in "$@" ; do case "$arg" in --debug) @@ -85,7 +99,7 @@ else SUDO= fi -if [ $(basename $0) = "letsencrypt-auto" ]; then +if [ $BASENAME = "letsencrypt-auto" ]; then # letsencrypt-auto does not respect --help or --yes for backwards compatibility ASSUME_YES=1 HELP=0 From c66f0bd18e9d65a48935eaf9d0c9ef630aa0f218 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Thu, 21 Apr 2016 16:13:17 -0400 Subject: [PATCH 036/396] Make le-auto helpful --- letsencrypt-auto-source/letsencrypt-auto | 8 ++++++-- letsencrypt-auto-source/letsencrypt-auto.template | 8 ++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/letsencrypt-auto-source/letsencrypt-auto b/letsencrypt-auto-source/letsencrypt-auto index df4783a72..9be546584 100755 --- a/letsencrypt-auto-source/letsencrypt-auto +++ b/letsencrypt-auto-source/letsencrypt-auto @@ -21,11 +21,11 @@ VENV_PATH=${VENV_PATH:-"$XDG_DATA_HOME/$VENV_NAME"} VENV_BIN="$VENV_PATH/bin" LE_AUTO_VERSION="0.6.0.dev0" BASENAME=$(basename $0) -USAGE="Usage: $BASENAME [OPTION] +USAGE="Usage: $BASENAME [OPTIONS] A self-updating wrapper script for the Certbot ACME client. When run, updates to both this script and certbot will be downloaded and installed. After ensuring you have the latest versions installed, certbot will be invoked with -all arguments you provided. +all arguments you have provided. Help for certbot itself cannot be provided until it is installed. @@ -860,6 +860,10 @@ else # package). Phase 2 checks the version of the locally installed certbot. if [ ! -f "$VENV_BIN/letsencrypt" ]; then + if [ "$HELP" = 1 ]; then + echo "$USAGE" + exit 0 + fi # If it looks like we've never bootstrapped before, bootstrap: Bootstrap fi diff --git a/letsencrypt-auto-source/letsencrypt-auto.template b/letsencrypt-auto-source/letsencrypt-auto.template index a17d11fa4..fe4baeafd 100755 --- a/letsencrypt-auto-source/letsencrypt-auto.template +++ b/letsencrypt-auto-source/letsencrypt-auto.template @@ -21,11 +21,11 @@ VENV_PATH=${VENV_PATH:-"$XDG_DATA_HOME/$VENV_NAME"} VENV_BIN="$VENV_PATH/bin" LE_AUTO_VERSION="{{ LE_AUTO_VERSION }}" BASENAME=$(basename $0) -USAGE="Usage: $BASENAME [OPTION] +USAGE="Usage: $BASENAME [OPTIONS] A self-updating wrapper script for the Certbot ACME client. When run, updates to both this script and certbot will be downloaded and installed. After ensuring you have the latest versions installed, certbot will be invoked with -all arguments you provided. +all arguments you have provided. Help for certbot itself cannot be provided until it is installed. @@ -256,6 +256,10 @@ else # package). Phase 2 checks the version of the locally installed certbot. if [ ! -f "$VENV_BIN/letsencrypt" ]; then + if [ "$HELP" = 1 ]; then + echo "$USAGE" + exit 0 + fi # If it looks like we've never bootstrapped before, bootstrap: Bootstrap fi From 2f81a8963e3038a5156ff660f663d55c1e3295ab Mon Sep 17 00:00:00 2001 From: Noah Swartz Date: Thu, 21 Apr 2016 15:18:27 -0700 Subject: [PATCH 037/396] move github refs back to LE --- docs/api/cb_util.rst | 5 ----- docs/api/le_util.rst | 5 +++++ docs/ciphers.rst | 4 ++-- docs/contributing.rst | 12 ++++++------ docs/using.rst | 14 +++++++------- 5 files changed, 20 insertions(+), 20 deletions(-) delete mode 100644 docs/api/cb_util.rst create mode 100644 docs/api/le_util.rst diff --git a/docs/api/cb_util.rst b/docs/api/cb_util.rst deleted file mode 100644 index 066fa906c..000000000 --- a/docs/api/cb_util.rst +++ /dev/null @@ -1,5 +0,0 @@ -:mod:`certbot.cb_util` --------------------------- - -.. automodule:: certbot.cb_util - :members: diff --git a/docs/api/le_util.rst b/docs/api/le_util.rst new file mode 100644 index 000000000..c9e332745 --- /dev/null +++ b/docs/api/le_util.rst @@ -0,0 +1,5 @@ +:mod:`certbot.le_util` +-------------------------- + +.. automodule:: certbot.le_util + :members: diff --git a/docs/ciphers.rst b/docs/ciphers.rst index be6784276..a37caa0d2 100644 --- a/docs/ciphers.rst +++ b/docs/ciphers.rst @@ -153,7 +153,7 @@ recommendations with sources of expert guidance on ciphersuites and other cryptographic parameters. We're grateful to everyone who contributed suggestions. The recommendations we received are available at -https://github.com/certbot/certbot/wiki/Ciphersuite-guidance +https://github.com/letsencrypt/letsencrypt/wiki/Ciphersuite-guidance Certbot users are welcome to review these authorities to better inform their own cryptographic parameter choices. We also @@ -196,7 +196,7 @@ TODO The status of this feature is tracked as part of issue #1123 in our bug tracker. -https://github.com/certbot/certbot/issues/1123 +https://github.com/letsencrypt/letsencrypt/issues/1123 Prior to implementation of #1123, the client does not actually modify ciphersuites (this is intended to be implemented as a "configuration diff --git a/docs/contributing.rst b/docs/contributing.rst index 3225de694..9f7a7b3c3 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -57,8 +57,8 @@ your pull request must have thorough unit test coverage, pass our `integration`_ tests, and be compliant with the :ref:`coding style `. -.. _github issue tracker: https://github.com/certbot/certbot/issues -.. _Good Volunteer Task: https://github.com/certbot/certbot/issues?q=is%3Aopen+is%3Aissue+label%3A%22Good+Volunteer+Task%22 +.. _github issue tracker: https://github.com/letsencrypt/letsencrypt/issues +.. _Good Volunteer Task: https://github.com/letsencrypt/letsencrypt/issues?q=is%3Aopen+is%3Aissue+label%3A%22Good+Volunteer+Task%22 Testing ------- @@ -146,7 +146,7 @@ encouraged) make sure to install prerequisites as listed in ``certbot-nginx/tests/boulder-integration.sh`` and rerun the integration tests suite. -.. _Boulder: https://github.com/certbot/boulder +.. _Boulder: https://github.com/letsencrypt/boulder .. _Go: https://golang.org @@ -175,8 +175,8 @@ Configurators may implement just one of those). There are also `~certbot.interfaces.IDisplay` plugins, which implement bindings to alternative UI libraries. -.. _interfaces.py: https://github.com/certbot/certbot/blob/master/certbot/interfaces.py -.. _plugins/common.py: https://github.com/certbot/certbot/blob/master/certbot/plugins/common.py#L34 +.. _interfaces.py: https://github.com/letsencrypt/letsencrypt/blob/master/letsencrypt/interfaces.py +.. _plugins/common.py: https://github.com/letsencrypt/letsencrypt/blob/master/letsencrypt/plugins/common.py#L34 Authenticators @@ -323,7 +323,7 @@ Steps: See `Known Issues`_. If it's not a known issue, fix any errors. .. _Known Issues: - https://github.com/certbot/certbot/wiki/Known-issues + https://github.com/letsencrypt/letsencrypt/wiki/Known-issues Updating the documentation ========================== diff --git a/docs/using.rst b/docs/using.rst index 2b16e9a27..94a4af72d 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -93,10 +93,10 @@ s3front_ Y Y Integration with Amazon CloudFront distribution of S3 buck gandi_ Y Y Integration with Gandi's hosting products and API =========== ==== ==== =============================================================== -.. _plesk: https://github.com/plesk/certbot-plesk -.. _haproxy: https://code.greenhost.net/open/certbot-haproxy -.. _s3front: https://github.com/dlapiduz/certbot-s3front -.. _gandi: https://github.com/Gandi/certbot-gandi +.. _plesk: https://github.com/plesk/letsencrypt-plesk +.. _haproxy: https://code.greenhost.net/open/letsencrypt-haproxy +.. _s3front: https://github.com/dlapiduz/letsencrypt-s3front +.. _gandi: https://github.com/Gandi/letsencrypt-gandi Future plugins for IMAP servers, SMTP servers, IRC servers, etc, are likely to be installers but not authenticators. @@ -194,7 +194,7 @@ Third-party plugins ------------------- These plugins are listed at -https://github.com/certbot/certbot/wiki/Plugins. If you're +https://github.com/letsencrypt/letsencrypt/wiki/Plugins. If you're interested, you can also :ref:`write your own plugin `. Renewal @@ -365,7 +365,7 @@ get support on our `forums `_. If you find a bug in the software, please do report it in our `issue tracker -`_. Remember to +`_. Remember to give us as much information as possible: - copy and paste exact command line used and the output (though mind @@ -390,7 +390,7 @@ plugins cannot reach it from inside the Docker container. You should definitely read the :ref:`where-certs` section, in order to know how to manage the certs -manually. https://github.com/certbot/certbot/wiki/Ciphersuite-guidance +manually. https://github.com/letsencrypt/letsencrypt/wiki/Ciphersuite-guidance provides some information about recommended ciphersuites. If none of these make much sense to you, you should definitely use the certbot-auto_ method, which enables you to use installer plugins From d803fb9d2a8d30e2485bf9742e57e2d800501945 Mon Sep 17 00:00:00 2001 From: Noah Swartz Date: Thu, 21 Apr 2016 15:42:02 -0700 Subject: [PATCH 038/396] fix /etc/ and 3p --- docs/using.rst | 60 +++++++++++++++++++++++++------------------------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/docs/using.rst b/docs/using.rst index 94a4af72d..07a3b6b6c 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -27,9 +27,9 @@ To install and run the client, just type... ./certbot-auto -.. hint:: During the beta phase, Certbot enforces strict rate limits on - the number of certificates issued for one domain. It is recommended to - initially use the test server via `--test-cert` until you get the desired +.. hint:: During the beta phase, the Let's Encrypt servers enforce strict rate + limits on the number of certificates issued for one domain. It is recommended + to initially use the test server via `--test-cert` until you get the desired certificates. Throughout the documentation, whenever you see references to @@ -137,14 +137,14 @@ would obtain a single certificate for all of those names, using the ``/var/www/other`` for the second two. The webroot plugin works by creating a temporary file for each of your requested -domains in ``${webroot-path}/.well-known/acme-challenge``. Then the Certbot +domains in ``${webroot-path}/.well-known/acme-challenge``. Then the Let's Encrypt validation server makes HTTP requests to validate that the DNS for each requested domain resolves to the server running certbot. An example request made to your web server would look like: :: - 66.133.109.36 - - [05/Jan/2016:20:11:24 -0500] "GET /.well-known/acme-challenge/HGr8U1IeTW4kY_Z6UIyaakzOkyQgPr_7ArlLgtZE8SX HTTP/1.1" 200 87 "-" "Mozilla/5.0 (compatible; Certbot validation server; +https://www.certbot.com)" + 66.133.109.36 - - [05/Jan/2016:20:11:24 -0500] "GET /.well-known/acme-challenge/HGr8U1IeTW4kY_Z6UIyaakzOkyQgPr_7ArlLgtZE8SX HTTP/1.1" 200 87 "-" "Mozilla/5.0 (compatible; Let's Encryptvalidation server; +https://www.letsencrypt.org)" Note that to use the webroot plugin, your server must be configured to serve files from hidden directories. If ``/.well-known`` is treated specially by @@ -272,14 +272,14 @@ you prefer to manage everything by hand, this section provides information on where to find necessary files. All generated keys and issued certificates can be found in -``/etc/certbot/live/$domain``. Rather than copying, please point +``/etc/letsencrypt/live/$domain``. Rather than copying, please point your (web) server configuration directly to those files (or create -symlinks). During the renewal_, ``/etc/certbot/live`` is updated +symlinks). During the renewal_, ``/etc/letsencrypt/live`` is updated with the latest necessary files. -.. note:: ``/etc/certbot/archive`` and ``/etc/certbot/keys`` +.. note:: ``/etc/letsencrypt/archive`` and ``/etc/letsencrypt/keys`` contain all previous keys and certificates, while - ``/etc/certbot/live`` symlinks to the latest versions. + ``/etc/letsencrypt/live`` symlinks to the latest versions. The following files are available: @@ -348,9 +348,9 @@ example configuration file is shown below: By default, the following locations are searched: -- ``/etc/certbot/cli.ini`` -- ``$XDG_CONFIG_HOME/certbot/cli.ini`` (or - ``~/.config/certbot/cli.ini`` if ``$XDG_CONFIG_HOME`` is not +- ``/etc/letsencrypt/cli.ini`` +- ``$XDG_CONFIG_HOME/letsencrypt/cli.ini`` (or + ``~/.config/letsencrypt/cli.ini`` if ``$XDG_CONFIG_HOME`` is not set). .. keep it up to date with constants.py @@ -361,7 +361,7 @@ Getting help If you're having problems you can chat with us on `IRC (#certbot @ OFTC) `_ or -get support on our `forums `_. +get support on our `forums `_. If you find a bug in the software, please do report it in our `issue tracker @@ -371,7 +371,7 @@ give us as much information as possible: - copy and paste exact command line used and the output (though mind that the latter might include some personally identifiable information, including your email and domains) -- copy and paste logs from ``/var/log/certbot`` (though mind they +- copy and paste logs from ``/var/log/letsencrypt`` (though mind they also might contain personally identifiable information) - copy and paste ``certbot --version`` output - your operating system, including specific version @@ -403,13 +403,13 @@ to, `install Docker`_, then issue the following command: .. code-block:: shell sudo docker run -it --rm -p 443:443 -p 80:80 --name certbot \ - -v "/etc/certbot:/etc/certbot" \ - -v "/var/lib/certbot:/var/lib/certbot" \ - quay.io/certbot/certbot:latest auth + -v "/etc/letsencrypt:/etc/letsencrypt" \ + -v "/var/lib/letsencrypt:/var/lib/letsencrypt" \ + quay.io/letsencrypt/letsencrypt:latest auth and follow the instructions (note that ``auth`` command is explicitly used - no installer plugins involved). Your new cert will be available -in ``/etc/certbot/live`` on the host. +in ``/etc/letsencrypt/live`` on the host. .. _Docker: https://docker.com .. _`install Docker`: https://docs.docker.com/userguide/ @@ -420,31 +420,31 @@ Operating System Packages **FreeBSD** - * Port: ``cd /usr/ports/security/py-certbot make install clean`` - * Package: ``pkg install py27-certbot`` + * Port: ``cd /usr/ports/security/py-letsencrypt make install clean`` + * Package: ``pkg install py27-letsencrypt`` **OpenBSD** - * Port: ``cd /usr/ports/security/certbot/client && make install clean`` - * Package: ``pkg_add certbot`` + * Port: ``cd /usr/ports/security/letsencrypt/client && make install clean`` + * Package: ``pkg_add letsencrypt`` **Arch Linux** .. code-block:: shell - sudo pacman -S certbot certbot-apache + sudo pacman -S letsencrypt letsencrypt-apache **Debian** -If you run Debian Stretch or Debian Sid, you can install certbot packages. +If you run Debian Stretch or Debian Sid, you can install letsencrypt packages. .. code-block:: shell sudo apt-get update - sudo apt-get install certbot python-certbot-apache + sudo apt-get install letsencrypt python-letsencrypt-apache If you don't want to use the Apache plugin, you can omit the -``python-certbot-apache`` package. +``python-letsencrypt-apache`` package. Packages for Debian Jessie are coming in the next few weeks. @@ -452,7 +452,7 @@ Packages for Debian Jessie are coming in the next few weeks. .. code-block:: shell - sudo dnf install certbot + sudo dnf install letsencrypt **Gentoo** @@ -461,8 +461,8 @@ want to use the Apache plugin, it has to be installed separately: .. code-block:: shell - emerge -av app-crypt/certbot - emerge -av app-crypt/certbot-apache + emerge -av app-crypt/letsencrypt + emerge -av app-crypt/letsencrypt-apache Currently, only the Apache plugin is included in Portage. However, if you want the nginx plugin, you can use Layman to add the mrueg overlay which @@ -473,7 +473,7 @@ does include the nginx plugin package: emerge -av app-portage/layman layman -S layman -a mrueg - emerge -av app-crypt/certbot-nginx + emerge -av app-crypt/letsencrypt-nginx When using the Apache plugin, you will run into a "cannot find a cert or key directive" error if you're sporting the default Gentoo ``httpd.conf``. From 2869f06109951a393134a868a02226d9f0569a1e Mon Sep 17 00:00:00 2001 From: Noah Swartz Date: Thu, 21 Apr 2016 15:56:23 -0700 Subject: [PATCH 039/396] add README.rst --- README.rst | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/README.rst b/README.rst index 050cde82b..7c7269dba 100644 --- a/README.rst +++ b/README.rst @@ -3,7 +3,7 @@ Disclaimer ========== -The Let's Encrypt Client is **BETA SOFTWARE**. It contains plenty of bugs and +The Certbot is **BETA SOFTWARE**. It contains plenty of bugs and rough edges, and should be tested thoroughly in staging environments before use on production systems. @@ -11,10 +11,10 @@ For more information regarding the status of the project, please see https://letsencrypt.org. Be sure to checkout the `Frequently Asked Questions (FAQ) `_. -About the Let's Encrypt Client +About Certbot ============================== -The Let's Encrypt Client is a fully-featured, extensible client for the Let's +Certbot is a fully-featured, extensible client for the Let's Encrypt CA (or any other CA that speaks the `ACME `_ protocol) that can automate the tasks of obtaining certificates and @@ -24,22 +24,22 @@ systems. Installation ------------ -If ``letsencrypt`` is packaged for your Unix OS, you can install it from -there, and run it by typing ``letsencrypt``. Because not all operating -systems have packages yet, we provide a temporary solution via the -``letsencrypt-auto`` wrapper script, which obtains some dependencies -from your OS and puts others in a python virtual environment:: +If ``certbot`` (or ``letsencrypt``) is packaged for your Unix OS, you can install +it from there, and run it by typing ``certbot`` (or ``letsencrypt``). +Because not all operating systems have packages yet, we provide a temporary +solution via the ``certbot-auto`` wrapper script, which obtains some +dependencies from your OS and puts others in a python virtual environment:: user@webserver:~$ git clone https://github.com/letsencrypt/letsencrypt user@webserver:~$ cd letsencrypt - user@webserver:~/letsencrypt$ ./letsencrypt-auto --help + user@webserver:~/letsencrypt$ ./certbot-auto --help Or for full command line help, type:: - ./letsencrypt-auto --help all + ./certbot-auto --help all -``letsencrypt-auto`` updates to the latest client release automatically. And -since ``letsencrypt-auto`` is a wrapper to ``letsencrypt``, it accepts exactly +``certbot-auto`` updates to the latest client release automatically. And +since ``certbot-auto`` is a wrapper to ``certbot``, it accepts exactly the same command line flags and arguments. More details about this script and other installation methods can be found `in the User Guide `_. @@ -47,7 +47,7 @@ other installation methods can be found `in the User Guide How to run the client --------------------- -In many cases, you can just run ``letsencrypt-auto`` or ``letsencrypt``, and the +In many cases, you can just run ``certbot-auto`` or ``certbot``, and the client will guide you through the process of obtaining and installing certs interactively. @@ -56,7 +56,7 @@ For instance, if you want to obtain a cert for ``example.com``, ``www.example.com``, and ``other.example.net``, using the Apache plugin to both obtain and install the certs, you could do this:: - ./letsencrypt-auto --apache -d example.com -d www.example.com -d other.example.net + ./certbot-auto --apache -d example.com -d www.example.com -d other.example.net (The first time you run the command, it will make an account, and ask for an email and agreement to the Let's Encrypt Subscriber Agreement; you can @@ -65,7 +65,7 @@ automate those with ``--email`` and ``--agree-tos``) If you want to use a webserver that doesn't have full plugin support yet, you can still use "standalone" or "webroot" plugins to obtain a certificate:: - ./letsencrypt-auto certonly --standalone --email admin@example.com -d example.com -d www.example.com -d other.example.net + ./certbot-auto certonly --standalone --email admin@example.com -d example.com -d www.example.com -d other.example.net Understanding the client in more depth @@ -87,7 +87,7 @@ Notes for developers: https://letsencrypt.readthedocs.org/en/latest/contributing Main Website: https://letsencrypt.org/ -IRC Channel: #letsencrypt on `Freenode`_ +IRC Channel: #letsencrypt on `Freenode`_ or #certbot on `OFTC`_ Community: https://community.letsencrypt.org @@ -152,7 +152,7 @@ Current Features - standalone (runs its own simple webserver to prove you control a domain) - webroot (adds files to webroot directories in order to prove control of domains and obtain certs) - - nginx/0.8.48+ (highly experimental, not included in letsencrypt-auto) + - nginx/0.8.48+ (highly experimental, not included in certbot-auto) * The private key is generated locally on your system. * Can talk to the Let's Encrypt CA or optionally to other ACME From 61203db2ebd17a27cb0f3316812bb5d72683c546 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Fri, 22 Apr 2016 13:01:32 -0400 Subject: [PATCH 040/396] Add --yes support to arch and debian bootstrappers --- letsencrypt-auto-source/letsencrypt-auto | 14 +++++++++++--- .../pieces/bootstrappers/arch_common.sh | 6 +++++- .../pieces/bootstrappers/deb_common.sh | 8 ++++++-- 3 files changed, 22 insertions(+), 6 deletions(-) diff --git a/letsencrypt-auto-source/letsencrypt-auto b/letsencrypt-auto-source/letsencrypt-auto index 9be546584..d96836e64 100755 --- a/letsencrypt-auto-source/letsencrypt-auto +++ b/letsencrypt-auto-source/letsencrypt-auto @@ -175,6 +175,10 @@ BootstrapDebCommon() { augeas_pkg="libaugeas0 augeas-lenses" AUGVERSION=`apt-cache show --no-all-versions libaugeas0 | grep ^Version: | cut -d" " -f2` + if [ "$ASSUME_YES" = 1 ]; then + YES_FLAG="-y" + fi + AddBackportRepo() { # ARGS: BACKPORT_NAME="$1" @@ -196,7 +200,7 @@ BootstrapDebCommon() { $SUDO apt-get update fi fi - $SUDO apt-get install -y --no-install-recommends -t "$BACKPORT_NAME" $augeas_pkg + $SUDO apt-get install $YES_FLAG --no-install-recommends -t "$BACKPORT_NAME" $augeas_pkg augeas_pkg= } @@ -215,7 +219,7 @@ BootstrapDebCommon() { # XXX add a case for ubuntu PPAs fi - $SUDO apt-get install -y --no-install-recommends \ + $SUDO apt-get install $YES_FLAG --no-install-recommends \ python \ python-dev \ $virtualenv \ @@ -334,8 +338,12 @@ BootstrapArchCommon() { # pacman -T exits with 127 if there are missing dependencies missing=$($SUDO pacman -T $deps) || true + if [ "$ASSUME_YES" = 1 ]; then + noconfirm="--noconfirm" + fi + if [ "$missing" ]; then - $SUDO pacman -S --needed $missing + $SUDO pacman -S --needed $missing $noconfirm fi } diff --git a/letsencrypt-auto-source/pieces/bootstrappers/arch_common.sh b/letsencrypt-auto-source/pieces/bootstrappers/arch_common.sh index b2fc01a14..39e2da5fe 100755 --- a/letsencrypt-auto-source/pieces/bootstrappers/arch_common.sh +++ b/letsencrypt-auto-source/pieces/bootstrappers/arch_common.sh @@ -21,7 +21,11 @@ BootstrapArchCommon() { # pacman -T exits with 127 if there are missing dependencies missing=$($SUDO pacman -T $deps) || true + if [ "$ASSUME_YES" = 1 ]; then + noconfirm="--noconfirm" + fi + if [ "$missing" ]; then - $SUDO pacman -S --needed $missing + $SUDO pacman -S --needed $missing $noconfirm fi } diff --git a/letsencrypt-auto-source/pieces/bootstrappers/deb_common.sh b/letsencrypt-auto-source/pieces/bootstrappers/deb_common.sh index 57ed11399..b2b76fd55 100644 --- a/letsencrypt-auto-source/pieces/bootstrappers/deb_common.sh +++ b/letsencrypt-auto-source/pieces/bootstrappers/deb_common.sh @@ -34,6 +34,10 @@ BootstrapDebCommon() { augeas_pkg="libaugeas0 augeas-lenses" AUGVERSION=`apt-cache show --no-all-versions libaugeas0 | grep ^Version: | cut -d" " -f2` + if [ "$ASSUME_YES" = 1 ]; then + YES_FLAG="-y" + fi + AddBackportRepo() { # ARGS: BACKPORT_NAME="$1" @@ -55,7 +59,7 @@ BootstrapDebCommon() { $SUDO apt-get update fi fi - $SUDO apt-get install -y --no-install-recommends -t "$BACKPORT_NAME" $augeas_pkg + $SUDO apt-get install $YES_FLAG --no-install-recommends -t "$BACKPORT_NAME" $augeas_pkg augeas_pkg= } @@ -74,7 +78,7 @@ BootstrapDebCommon() { # XXX add a case for ubuntu PPAs fi - $SUDO apt-get install -y --no-install-recommends \ + $SUDO apt-get install $YES_FLAG --no-install-recommends \ python \ python-dev \ $virtualenv \ From 40aa4dbf91349f5ca7a191d5f00f55b49f6e92af Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Fri, 22 Apr 2016 14:51:40 -0400 Subject: [PATCH 041/396] add --yes support to red hat bootstrap script --- letsencrypt-auto-source/letsencrypt-auto | 78 ++++++++++--------- .../pieces/bootstrappers/rpm_common.sh | 78 ++++++++++--------- 2 files changed, 82 insertions(+), 74 deletions(-) diff --git a/letsencrypt-auto-source/letsencrypt-auto b/letsencrypt-auto-source/letsencrypt-auto index d96836e64..9e1dfef42 100755 --- a/letsencrypt-auto-source/letsencrypt-auto +++ b/letsencrypt-auto-source/letsencrypt-auto @@ -240,9 +240,10 @@ BootstrapDebCommon() { BootstrapRpmCommon() { # Tested with: - # - Fedora 22, 23 (x64) + # - Fedora 20, 21, 22, 23 (x64) # - Centos 7 (x64: on DigitalOcean droplet) # - CentOS 7 Minimal install in a Hyper-V VM + # - CentOS 6 (EPEL must be installed manually) if type dnf 2>/dev/null then @@ -256,47 +257,50 @@ BootstrapRpmCommon() { exit 1 fi + pkgs=" + gcc + dialog + augeas-libs + openssl + openssl-devel + libffi-devel + redhat-rpm-config + ca-certificates + " + # Some distros and older versions of current distros use a "python27" # instead of "python" naming convention. Try both conventions. - if ! $SUDO $tool install -y \ - python \ - python-devel \ - python-virtualenv \ - python-tools \ - python-pip - then - if ! $SUDO $tool install -y \ - python27 \ - python27-devel \ - python27-virtualenv \ - python27-tools \ - python27-pip - then - echo "Could not install Python dependencies. Aborting bootstrap!" - exit 1 - fi + if $SUDO $tool list python >/dev/null 2>&1; then + pkgs="$pkgs + python + python-devel + python-virtualenv + python-tools + python-pip + " + else + pkgs="$pkgs + python27 + python27-devel + python27-virtualenv + python27-tools + python27-pip + " fi - if ! $SUDO $tool install -y \ - gcc \ - dialog \ - augeas-libs \ - openssl \ - openssl-devel \ - libffi-devel \ - redhat-rpm-config \ - ca-certificates - then - echo "Could not install additional dependencies. Aborting bootstrap!" - exit 1 - fi - - if $SUDO $tool list installed "httpd" >/dev/null 2>&1; then - if ! $SUDO $tool install -y mod_ssl - then - echo "Apache found, but mod_ssl could not be installed." - fi + pkgs="$pkgs + mod_ssl + " + fi + + if [ "$ASSUME_YES" = 1 ]; then + yes_flag="-y" + fi + + if ! $SUDO $tool install $yes_flag $pkgs; then + echo "Could not install OS dependencies. Aborting bootstrap!" + exit 1 fi } diff --git a/letsencrypt-auto-source/pieces/bootstrappers/rpm_common.sh b/letsencrypt-auto-source/pieces/bootstrappers/rpm_common.sh index 68a11a531..0f98b4bbc 100755 --- a/letsencrypt-auto-source/pieces/bootstrappers/rpm_common.sh +++ b/letsencrypt-auto-source/pieces/bootstrappers/rpm_common.sh @@ -1,8 +1,9 @@ BootstrapRpmCommon() { # Tested with: - # - Fedora 22, 23 (x64) + # - Fedora 20, 21, 22, 23 (x64) # - Centos 7 (x64: on DigitalOcean droplet) # - CentOS 7 Minimal install in a Hyper-V VM + # - CentOS 6 (EPEL must be installed manually) if type dnf 2>/dev/null then @@ -16,46 +17,49 @@ BootstrapRpmCommon() { exit 1 fi + pkgs=" + gcc + dialog + augeas-libs + openssl + openssl-devel + libffi-devel + redhat-rpm-config + ca-certificates + " + # Some distros and older versions of current distros use a "python27" # instead of "python" naming convention. Try both conventions. - if ! $SUDO $tool install -y \ - python \ - python-devel \ - python-virtualenv \ - python-tools \ - python-pip - then - if ! $SUDO $tool install -y \ - python27 \ - python27-devel \ - python27-virtualenv \ - python27-tools \ - python27-pip - then - echo "Could not install Python dependencies. Aborting bootstrap!" - exit 1 - fi + if $SUDO $tool list python >/dev/null 2>&1; then + pkgs="$pkgs + python + python-devel + python-virtualenv + python-tools + python-pip + " + else + pkgs="$pkgs + python27 + python27-devel + python27-virtualenv + python27-tools + python27-pip + " fi - if ! $SUDO $tool install -y \ - gcc \ - dialog \ - augeas-libs \ - openssl \ - openssl-devel \ - libffi-devel \ - redhat-rpm-config \ - ca-certificates - then - echo "Could not install additional dependencies. Aborting bootstrap!" - exit 1 - fi - - if $SUDO $tool list installed "httpd" >/dev/null 2>&1; then - if ! $SUDO $tool install -y mod_ssl - then - echo "Apache found, but mod_ssl could not be installed." - fi + pkgs="$pkgs + mod_ssl + " + fi + + if [ "$ASSUME_YES" = 1 ]; then + yes_flag="-y" + fi + + if ! $SUDO $tool install $yes_flag $pkgs; then + echo "Could not install OS dependencies. Aborting bootstrap!" + exit 1 fi } From ab2319e6097beab3b05e5428b8185a6e1b330e5e Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Fri, 22 Apr 2016 15:00:24 -0400 Subject: [PATCH 042/396] Respect yes with opensuse bootstrap --- letsencrypt-auto-source/letsencrypt-auto | 7 ++++++- .../pieces/bootstrappers/suse_common.sh | 7 ++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/letsencrypt-auto-source/letsencrypt-auto b/letsencrypt-auto-source/letsencrypt-auto index 9e1dfef42..dfa39b6d1 100755 --- a/letsencrypt-auto-source/letsencrypt-auto +++ b/letsencrypt-auto-source/letsencrypt-auto @@ -307,7 +307,12 @@ BootstrapRpmCommon() { BootstrapSuseCommon() { # SLE12 don't have python-virtualenv - $SUDO zypper -nq in -l \ + if [ "$ASSUME_YES" = 1 ]; then + zypper_flags="-nq" + install_flags="-l" + fi + + $SUDO zypper $zypper_flags in $install_flags \ python \ python-devel \ python-virtualenv \ diff --git a/letsencrypt-auto-source/pieces/bootstrappers/suse_common.sh b/letsencrypt-auto-source/pieces/bootstrappers/suse_common.sh index 46c60f992..9ac295922 100755 --- a/letsencrypt-auto-source/pieces/bootstrappers/suse_common.sh +++ b/letsencrypt-auto-source/pieces/bootstrappers/suse_common.sh @@ -1,7 +1,12 @@ BootstrapSuseCommon() { # SLE12 don't have python-virtualenv - $SUDO zypper -nq in -l \ + if [ "$ASSUME_YES" = 1 ]; then + zypper_flags="-nq" + install_flags="-l" + fi + + $SUDO zypper $zypper_flags in $install_flags \ python \ python-devel \ python-virtualenv \ From 23baf225a4ee9fb9382381290781f9d9c124ac20 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Fri, 22 Apr 2016 16:44:06 -0400 Subject: [PATCH 043/396] Ask before enabling backports --- letsencrypt-auto-source/letsencrypt-auto | 34 ++++++++++++------- .../pieces/bootstrappers/deb_common.sh | 34 ++++++++++++------- 2 files changed, 42 insertions(+), 26 deletions(-) diff --git a/letsencrypt-auto-source/letsencrypt-auto b/letsencrypt-auto-source/letsencrypt-auto index dfa39b6d1..3ca88c279 100755 --- a/letsencrypt-auto-source/letsencrypt-auto +++ b/letsencrypt-auto-source/letsencrypt-auto @@ -183,26 +183,34 @@ BootstrapDebCommon() { # ARGS: BACKPORT_NAME="$1" BACKPORT_SOURCELINE="$2" + echo "To use the Apache Certbot plugin, augeas needs to be installed from $BACKPORT_NAME." if ! grep -v -e ' *#' /etc/apt/sources.list | grep -q "$BACKPORT_NAME" ; then # This can theoretically error if sources.list.d is empty, but in that case we don't care. if ! grep -v -e ' *#' /etc/apt/sources.list.d/* 2>/dev/null | grep -q "$BACKPORT_NAME"; then - /bin/echo -n "Installing augeas from $BACKPORT_NAME in 3 seconds..." - sleep 1s - /bin/echo -ne "\e[0K\rInstalling augeas from $BACKPORT_NAME in 2 seconds..." - sleep 1s - /bin/echo -e "\e[0K\rInstalling augeas from $BACKPORT_NAME in 1 second ..." - sleep 1s if echo $BACKPORT_NAME | grep -q wheezy ; then - /bin/echo '(Backports are only installed if explicitly requested via "apt-get install -t wheezy-backports")' + echo 'Backports are only installed if explicitly requested via "apt-get install -t wheezy-backports"' + fi + if [ "$ASSUME_YES" = 1 ]; then + add_backports=1 + else + read -p "Would you like to enable the $BACKPORT_NAME repository [Y/n]? " response + case $response in + [yY][eE][sS]|[yY]|"") + add_backports=1;; + *) + add_backports=0;; + esac + fi + if [ "$add_backports" = 1 ]; then + $SUDO sh -c "echo $BACKPORT_SOURCELINE >> /etc/apt/sources.list.d/$BACKPORT_NAME.list" + $SUDO apt-get update fi - - $SUDO sh -c "echo $BACKPORT_SOURCELINE >> /etc/apt/sources.list.d/$BACKPORT_NAME.list" - $SUDO apt-get update fi fi - $SUDO apt-get install $YES_FLAG --no-install-recommends -t "$BACKPORT_NAME" $augeas_pkg - augeas_pkg= - + if [ "$add_backports" != 0 ]; then + $SUDO apt-get install $YES_FLAG --no-install-recommends -t "$BACKPORT_NAME" $augeas_pkg + augeas_pkg= + fi } diff --git a/letsencrypt-auto-source/pieces/bootstrappers/deb_common.sh b/letsencrypt-auto-source/pieces/bootstrappers/deb_common.sh index b2b76fd55..5370aa2f2 100644 --- a/letsencrypt-auto-source/pieces/bootstrappers/deb_common.sh +++ b/letsencrypt-auto-source/pieces/bootstrappers/deb_common.sh @@ -42,26 +42,34 @@ BootstrapDebCommon() { # ARGS: BACKPORT_NAME="$1" BACKPORT_SOURCELINE="$2" + echo "To use the Apache Certbot plugin, augeas needs to be installed from $BACKPORT_NAME." if ! grep -v -e ' *#' /etc/apt/sources.list | grep -q "$BACKPORT_NAME" ; then # This can theoretically error if sources.list.d is empty, but in that case we don't care. if ! grep -v -e ' *#' /etc/apt/sources.list.d/* 2>/dev/null | grep -q "$BACKPORT_NAME"; then - /bin/echo -n "Installing augeas from $BACKPORT_NAME in 3 seconds..." - sleep 1s - /bin/echo -ne "\e[0K\rInstalling augeas from $BACKPORT_NAME in 2 seconds..." - sleep 1s - /bin/echo -e "\e[0K\rInstalling augeas from $BACKPORT_NAME in 1 second ..." - sleep 1s if echo $BACKPORT_NAME | grep -q wheezy ; then - /bin/echo '(Backports are only installed if explicitly requested via "apt-get install -t wheezy-backports")' + echo 'Backports are only installed if explicitly requested via "apt-get install -t wheezy-backports"' + fi + if [ "$ASSUME_YES" = 1 ]; then + add_backports=1 + else + read -p "Would you like to enable the $BACKPORT_NAME repository [Y/n]? " response + case $response in + [yY][eE][sS]|[yY]|"") + add_backports=1;; + *) + add_backports=0;; + esac + fi + if [ "$add_backports" = 1 ]; then + $SUDO sh -c "echo $BACKPORT_SOURCELINE >> /etc/apt/sources.list.d/$BACKPORT_NAME.list" + $SUDO apt-get update fi - - $SUDO sh -c "echo $BACKPORT_SOURCELINE >> /etc/apt/sources.list.d/$BACKPORT_NAME.list" - $SUDO apt-get update fi fi - $SUDO apt-get install $YES_FLAG --no-install-recommends -t "$BACKPORT_NAME" $augeas_pkg - augeas_pkg= - + if [ "$add_backports" != 0 ]; then + $SUDO apt-get install $YES_FLAG --no-install-recommends -t "$BACKPORT_NAME" $augeas_pkg + augeas_pkg= + fi } From ff30fb71d2cabeae6951d3c77ed536fe1070c415 Mon Sep 17 00:00:00 2001 From: Joona Hoikkala Date: Fri, 29 Apr 2016 15:31:59 +0300 Subject: [PATCH 044/396] New method for determining UA string --- certbot/client.py | 2 +- certbot/le_util.py | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/certbot/client.py b/certbot/client.py index 60e37a787..4c90a84ca 100644 --- a/certbot/client.py +++ b/certbot/client.py @@ -52,7 +52,7 @@ def _determine_user_agent(config): if config.user_agent is None: ua = "CertbotACMEClient/{0} ({1}) Authenticator/{2} Installer/{3}" - ua = ua.format(certbot.__version__, " ".join(le_util.get_os_info()), + ua = ua.format(certbot.__version__, le_util.get_os_info_ua(), config.authenticator, config.installer) else: ua = config.user_agent diff --git a/certbot/le_util.py b/certbot/le_util.py index c1f4d1acd..fcde840c9 100644 --- a/certbot/le_util.py +++ b/certbot/le_util.py @@ -228,6 +228,24 @@ def get_os_info(filepath="/etc/os-release"): return get_python_os_info() +def get_os_info_ua(filepath="/etc/os-release"): + """ + Get OS name and version string for User Agent + + :param str filepath: File path of os-release file + :returns: os_ua + :rtype: `str` + """ + + if os.path.isfile(filepath): + os_ua = _get_systemd_os_release_var("NAME", filepath=filepath) + if os_ua: + return os_ua + + # Fallback + return " ".join(get_python_os_info()) + + def get_systemd_os_info(filepath="/etc/os-release"): """ Parse systemd /etc/os-release for distribution information From 1b5efc842719312fbb22cd19f27ab8011ace3f81 Mon Sep 17 00:00:00 2001 From: Joona Hoikkala Date: Fri, 29 Apr 2016 15:52:24 +0300 Subject: [PATCH 045/396] Added tests for new UA method --- certbot/tests/le_util_test.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/certbot/tests/le_util_test.py b/certbot/tests/le_util_test.py index 23ea40987..99aaba44e 100644 --- a/certbot/tests/le_util_test.py +++ b/certbot/tests/le_util_test.py @@ -344,23 +344,31 @@ class OsInfoTest(unittest.TestCase): """Test OS / distribution detection""" def test_systemd_os_release(self): - from certbot.le_util import get_os_info, get_systemd_os_info + from certbot.le_util import (get_os_info, get_systemd_os_info, + get_os_info_ua) + with mock.patch('os.path.isfile', return_value=True): self.assertEqual(get_os_info( test_util.vector_path("os-release"))[0], 'systemdos') self.assertEqual(get_os_info( test_util.vector_path("os-release"))[1], '42') self.assertEqual(get_systemd_os_info("/dev/null"), ("", "")) + self.assertEqual(get_os_info_ua( + test_util.vector_path("os-release")), + "SystemdOS") with mock.patch('os.path.isfile', return_value=False): self.assertEqual(get_systemd_os_info(), ("", "")) @mock.patch("certbot.le_util.subprocess.Popen") def test_non_systemd_os_info(self, popen_mock): - from certbot.le_util import get_os_info, get_python_os_info + from certbot.le_util import (get_os_info, get_python_os_info, + get_os_info_ua) with mock.patch('os.path.isfile', return_value=False): with mock.patch('platform.system_alias', return_value=('NonSystemD', '42', '42')): self.assertEqual(get_os_info()[0], 'nonsystemd') + self.assertEqual(get_os_info_ua(), + " ".join(get_python_os_info())) with mock.patch('platform.system_alias', return_value=('darwin', '', '')): From 9af0994ca6144daa45903e182f48e7036bd9047b Mon Sep 17 00:00:00 2001 From: Joona Hoikkala Date: Fri, 29 Apr 2016 16:23:52 +0300 Subject: [PATCH 046/396] More verbose UA & test UA test fixes --- certbot/le_util.py | 9 ++++++--- certbot/tests/cli_test.py | 4 ++-- certbot/tests/testdata/os-release | 1 - 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/certbot/le_util.py b/certbot/le_util.py index fcde840c9..d18032d9c 100644 --- a/certbot/le_util.py +++ b/certbot/le_util.py @@ -238,7 +238,9 @@ def get_os_info_ua(filepath="/etc/os-release"): """ if os.path.isfile(filepath): - os_ua = _get_systemd_os_release_var("NAME", filepath=filepath) + os_ua = _get_systemd_os_release_var("PRETTY_NAME", filepath=filepath) + if not os_ua: + os_ua = _get_systemd_os_release_var("NAME", filepath=filepath) if os_ua: return os_ua @@ -396,7 +398,7 @@ def enforce_domain_sanity(domain): domain = domain.encode('ascii').lower() except UnicodeError: error_fmt = (u"Internationalized domain names " - "are not presently supported: {0}") + "are not presently supported: {0}") if isinstance(domain, six.text_type): raise errors.ConfigurationError(error_fmt.format(domain)) else: @@ -423,5 +425,6 @@ def enforce_domain_sanity(domain): # first and last char is not "-" fqdn = re.compile("^((?!-)[A-Za-z0-9-]{1,63}(? Date: Mon, 2 May 2016 11:52:57 -0700 Subject: [PATCH 047/396] Add backports countdown when using --yes/letsencrypt-auto --- letsencrypt-auto-source/letsencrypt-auto | 6 ++++++ letsencrypt-auto-source/pieces/bootstrappers/deb_common.sh | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/letsencrypt-auto-source/letsencrypt-auto b/letsencrypt-auto-source/letsencrypt-auto index 3ca88c279..adeb94e3e 100755 --- a/letsencrypt-auto-source/letsencrypt-auto +++ b/letsencrypt-auto-source/letsencrypt-auto @@ -191,6 +191,12 @@ BootstrapDebCommon() { echo 'Backports are only installed if explicitly requested via "apt-get install -t wheezy-backports"' fi if [ "$ASSUME_YES" = 1 ]; then + /bin/echo -n "Installing augeas from $BACKPORT_NAME in 3 seconds..." + sleep 1s + /bin/echo -ne "\e[0K\rInstalling augeas from $BACKPORT_NAME in 2 seconds..." + sleep 1s + /bin/echo -e "\e[0K\rInstalling augeas from $BACKPORT_NAME in 1 second ..." + sleep 1s add_backports=1 else read -p "Would you like to enable the $BACKPORT_NAME repository [Y/n]? " response diff --git a/letsencrypt-auto-source/pieces/bootstrappers/deb_common.sh b/letsencrypt-auto-source/pieces/bootstrappers/deb_common.sh index 5370aa2f2..e91f52261 100644 --- a/letsencrypt-auto-source/pieces/bootstrappers/deb_common.sh +++ b/letsencrypt-auto-source/pieces/bootstrappers/deb_common.sh @@ -50,6 +50,12 @@ BootstrapDebCommon() { echo 'Backports are only installed if explicitly requested via "apt-get install -t wheezy-backports"' fi if [ "$ASSUME_YES" = 1 ]; then + /bin/echo -n "Installing augeas from $BACKPORT_NAME in 3 seconds..." + sleep 1s + /bin/echo -ne "\e[0K\rInstalling augeas from $BACKPORT_NAME in 2 seconds..." + sleep 1s + /bin/echo -e "\e[0K\rInstalling augeas from $BACKPORT_NAME in 1 second ..." + sleep 1s add_backports=1 else read -p "Would you like to enable the $BACKPORT_NAME repository [Y/n]? " response From 8f6c289e780903ce68b3b66d71a672b866b2e47e Mon Sep 17 00:00:00 2001 From: Noah Swartz Date: Mon, 2 May 2016 13:59:42 -0700 Subject: [PATCH 048/396] incorperate jsha's comments --- README.rst | 4 ++++ docs/contributing.rst | 2 +- docs/packaging.rst | 2 +- docs/using.rst | 11 ++++++----- 4 files changed, 12 insertions(+), 7 deletions(-) diff --git a/README.rst b/README.rst index 7c7269dba..a2af9ec59 100644 --- a/README.rst +++ b/README.rst @@ -21,6 +21,10 @@ protocol) that can automate the tasks of obtaining certificates and configuring webservers to use them. This client runs on Unix-based operating systems. +Until May 2016, Certbot was named simply ``letsencrypt`` or ``letsencrypt-auto``, +depending on install method. Instructions on the Internet, and some pieces of the +software, may still refer to this older name. + Installation ------------ diff --git a/docs/contributing.rst b/docs/contributing.rst index 9f7a7b3c3..60cc63e66 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -175,7 +175,7 @@ Configurators may implement just one of those). There are also `~certbot.interfaces.IDisplay` plugins, which implement bindings to alternative UI libraries. -.. _interfaces.py: https://github.com/letsencrypt/letsencrypt/blob/master/letsencrypt/interfaces.py +.. _interfaces.py: https://github.com/letsencrypt/letsencrypt/blob/master/certbot/interfaces.py .. _plugins/common.py: https://github.com/letsencrypt/letsencrypt/blob/master/letsencrypt/plugins/common.py#L34 diff --git a/docs/packaging.rst b/docs/packaging.rst index bd366dbaa..5f09b65fa 100644 --- a/docs/packaging.rst +++ b/docs/packaging.rst @@ -3,4 +3,4 @@ Packaging Guide =============== Documentation can be found at -https://github.com/certbot/certbot/wiki/Packaging. +https://github.com/letsencrypt/letsencrypt/wiki/Packaging. diff --git a/docs/using.rst b/docs/using.rst index 07a3b6b6c..56ce78e80 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -27,7 +27,7 @@ To install and run the client, just type... ./certbot-auto -.. hint:: During the beta phase, the Let's Encrypt servers enforce strict rate +.. hint:: The Let's Encrypt servers enforce rate limits on the number of certificates issued for one domain. It is recommended to initially use the test server via `--test-cert` until you get the desired certificates. @@ -144,7 +144,7 @@ made to your web server would look like: :: - 66.133.109.36 - - [05/Jan/2016:20:11:24 -0500] "GET /.well-known/acme-challenge/HGr8U1IeTW4kY_Z6UIyaakzOkyQgPr_7ArlLgtZE8SX HTTP/1.1" 200 87 "-" "Mozilla/5.0 (compatible; Let's Encryptvalidation server; +https://www.letsencrypt.org)" + 66.133.109.36 - - [05/Jan/2016:20:11:24 -0500] "GET /.well-known/acme-challenge/HGr8U1IeTW4kY_Z6UIyaakzOkyQgPr_7ArlLgtZE8SX HTTP/1.1" 200 87 "-" "Mozilla/5.0 (compatible; Let's Encrypt validation server; +https://www.letsencrypt.org)" Note that to use the webroot plugin, your server must be configured to serve files from hidden directories. If ``/.well-known`` is treated specially by @@ -360,8 +360,9 @@ Getting help ============ If you're having problems you can chat with us on `IRC (#certbot @ -OFTC) `_ or -get support on our `forums `_. +OFTC) `_ or at +`IRC (#letsencrypt @ freenode) `_ +or get support on our `forums `_. If you find a bug in the software, please do report it in our `issue tracker @@ -420,7 +421,7 @@ Operating System Packages **FreeBSD** - * Port: ``cd /usr/ports/security/py-letsencrypt make install clean`` + * Port: ``cd /usr/ports/security/py-letsencrypt && make install clean`` * Package: ``pkg install py27-letsencrypt`` **OpenBSD** From a9ecd146a9797eb068827bf6c357fde15f3a9a03 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 2 May 2016 14:16:41 -0700 Subject: [PATCH 049/396] Make certbot accept --yes flag --- certbot/cli.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/certbot/cli.py b/certbot/cli.py index e2c57595b..40af5dccb 100644 --- a/certbot/cli.py +++ b/certbot/cli.py @@ -649,6 +649,9 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): "automation", "--no-self-upgrade", action="store_true", help="(letsencrypt-auto only) prevent the letsencrypt-auto script from" " upgrading itself to newer released versions") + helpful.add( + "automation", "--yes", action="store_true", + help="(letsencrypt-auto only) assume yes is the answer to all prompts") helpful.add( "automation", "-q", "--quiet", dest="quiet", action="store_true", help="Silence all output except errors. Useful for automation via cron." From b844b7d605b2bcaa174cf4b025af926bffd1bc81 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Tue, 3 May 2016 15:44:36 -0700 Subject: [PATCH 050/396] Create certbot-auto during release process --- tools/release.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tools/release.sh b/tools/release.sh index d41192af9..06aefea44 100755 --- a/tools/release.sh +++ b/tools/release.sh @@ -188,9 +188,10 @@ while ! openssl dgst -sha256 -verify $RELEASE_OPENSSL_PUBKEY -signature \ done # copy leauto to the root, overwriting the previous release version +cp -p letsencrypt-auto-source/letsencrypt-auto certbot-auto cp -p letsencrypt-auto-source/letsencrypt-auto letsencrypt-auto -git add letsencrypt-auto letsencrypt-auto-source +git add certbot-auto letsencrypt-auto letsencrypt-auto-source git diff --cached git commit --gpg-sign="$RELEASE_GPG_KEY" -m "Release $version" git tag --local-user "$RELEASE_GPG_KEY" --sign --message "Release $version" "$tag" From 891b186856026da0d11e5916bfe78280ca7faa0d Mon Sep 17 00:00:00 2001 From: Noah Swartz Date: Tue, 3 May 2016 17:04:30 -0700 Subject: [PATCH 051/396] changed wiki refs, added some words --- docs/ciphers.rst | 6 +++--- docs/contributing.rst | 14 +++++++------- docs/packaging.rst | 2 +- docs/using.rst | 10 +++++----- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/docs/ciphers.rst b/docs/ciphers.rst index a37caa0d2..5a046e757 100644 --- a/docs/ciphers.rst +++ b/docs/ciphers.rst @@ -131,7 +131,7 @@ The Let's Encrypt project may deviate from the Mozilla recommendations in the future if good cause is shown and we believe our users' priorities would be well-served by doing so. In general, please address relevant proposals for changing priorities to the Mozilla security -team first, before asking the Let's Encrypt project to change +team first, before asking the Let's Encrypt project or EFF to change Certbot's priorities. The Mozilla security team is likely to have more resources and expertise to bring to bear on evaluating reasons why its recommendations should be updated. @@ -153,7 +153,7 @@ recommendations with sources of expert guidance on ciphersuites and other cryptographic parameters. We're grateful to everyone who contributed suggestions. The recommendations we received are available at -https://github.com/letsencrypt/letsencrypt/wiki/Ciphersuite-guidance +https://github.com/certbot/certbot/wiki/Ciphersuite-guidance Certbot users are welcome to review these authorities to better inform their own cryptographic parameter choices. We also @@ -196,7 +196,7 @@ TODO The status of this feature is tracked as part of issue #1123 in our bug tracker. -https://github.com/letsencrypt/letsencrypt/issues/1123 +https://github.com/certbot/certbot/issues/1123 Prior to implementation of #1123, the client does not actually modify ciphersuites (this is intended to be implemented as a "configuration diff --git a/docs/contributing.rst b/docs/contributing.rst index 60cc63e66..49e9e146d 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -20,8 +20,8 @@ once: .. code-block:: shell - git clone https://github.com/letsencrypt/letsencrypt - cd letsencrypt + git clone https://github.com/certbot/certbot + cd certbot ./letsencrypt-auto-source/letsencrypt-auto --os-packages-only ./tools/venv.sh @@ -57,8 +57,8 @@ your pull request must have thorough unit test coverage, pass our `integration`_ tests, and be compliant with the :ref:`coding style `. -.. _github issue tracker: https://github.com/letsencrypt/letsencrypt/issues -.. _Good Volunteer Task: https://github.com/letsencrypt/letsencrypt/issues?q=is%3Aopen+is%3Aissue+label%3A%22Good+Volunteer+Task%22 +.. _github issue tracker: https://github.com/certbot/certbot/issues +.. _Good Volunteer Task: https://github.com/certbot/certbot/issues?q=is%3Aopen+is%3Aissue+label%3A%22Good+Volunteer+Task%22 Testing ------- @@ -175,8 +175,8 @@ Configurators may implement just one of those). There are also `~certbot.interfaces.IDisplay` plugins, which implement bindings to alternative UI libraries. -.. _interfaces.py: https://github.com/letsencrypt/letsencrypt/blob/master/certbot/interfaces.py -.. _plugins/common.py: https://github.com/letsencrypt/letsencrypt/blob/master/letsencrypt/plugins/common.py#L34 +.. _interfaces.py: https://github.com/certbot/certbot/blob/master/certbot/interfaces.py +.. _plugins/common.py: https://github.com/certbot/certbot/blob/master/certbot/plugins/common.py#L34 Authenticators @@ -323,7 +323,7 @@ Steps: See `Known Issues`_. If it's not a known issue, fix any errors. .. _Known Issues: - https://github.com/letsencrypt/letsencrypt/wiki/Known-issues + https://github.com/certbot/certbot/wiki/Known-issues Updating the documentation ========================== diff --git a/docs/packaging.rst b/docs/packaging.rst index 5f09b65fa..bd366dbaa 100644 --- a/docs/packaging.rst +++ b/docs/packaging.rst @@ -3,4 +3,4 @@ Packaging Guide =============== Documentation can be found at -https://github.com/letsencrypt/letsencrypt/wiki/Packaging. +https://github.com/certbot/certbot/wiki/Packaging. diff --git a/docs/using.rst b/docs/using.rst index 56ce78e80..0dcab4971 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -194,7 +194,7 @@ Third-party plugins ------------------- These plugins are listed at -https://github.com/letsencrypt/letsencrypt/wiki/Plugins. If you're +https://github.com/certbot/certbot/wiki/Plugins. If you're interested, you can also :ref:`write your own plugin `. Renewal @@ -362,11 +362,11 @@ Getting help If you're having problems you can chat with us on `IRC (#certbot @ OFTC) `_ or at `IRC (#letsencrypt @ freenode) `_ -or get support on our `forums `_. +or get support on the Let's Encrypt `forums `_. If you find a bug in the software, please do report it in our `issue tracker -`_. Remember to +`_. Remember to give us as much information as possible: - copy and paste exact command line used and the output (though mind @@ -391,7 +391,7 @@ plugins cannot reach it from inside the Docker container. You should definitely read the :ref:`where-certs` section, in order to know how to manage the certs -manually. https://github.com/letsencrypt/letsencrypt/wiki/Ciphersuite-guidance +manually. https://github.com/certbot/certbot/wiki/Ciphersuite-guidance provides some information about recommended ciphersuites. If none of these make much sense to you, you should definitely use the certbot-auto_ method, which enables you to use installer plugins @@ -526,7 +526,7 @@ whole process is described in the :doc:`contributing`. Comparison of different methods ------------------------------- -Unless you have a very specific requirements, we kindly ask you to use +Unless you have a very specific requirements, we kindly suggest that you use the certbot-auto_ method. It's the fastest, the most thoroughly tested and the most reliable way of getting our software and the free SSL certificates! From e1d93663997bca079d7a415733a6fc9b11bbc803 Mon Sep 17 00:00:00 2001 From: Noah Swartz Date: Wed, 4 May 2016 10:52:58 -0700 Subject: [PATCH 052/396] updated README --- CHANGES.rst | 4 ++-- README.rst | 19 ++++++++++--------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 55e4bd796..4ce41a8bc 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -3,9 +3,9 @@ ChangeLog Please note: the change log will only get updated after first release - for now please use the -`commit log `_. +`commit log `_. To see the changes in a given release, inspect the github milestone for the release. For instance: -https://github.com/letsencrypt/letsencrypt/issues?utf8=%E2%9C%93&q=milestone%3A0.3.0 +https://github.com/certbot/certbot/issues?utf8=%E2%9C%93&q=milestone%3A0.3.0 diff --git a/README.rst b/README.rst index a2af9ec59..60e11901c 100644 --- a/README.rst +++ b/README.rst @@ -34,9 +34,9 @@ Because not all operating systems have packages yet, we provide a temporary solution via the ``certbot-auto`` wrapper script, which obtains some dependencies from your OS and puts others in a python virtual environment:: - user@webserver:~$ git clone https://github.com/letsencrypt/letsencrypt - user@webserver:~$ cd letsencrypt - user@webserver:~/letsencrypt$ ./certbot-auto --help + user@webserver:~$ git clone https://github.com/certbot/certbot + user@webserver:~$ cd certbot + user@webserver:~/certbot$ ./certbot-auto --help Or for full command line help, type:: @@ -85,7 +85,7 @@ Links Documentation: https://letsencrypt.readthedocs.org -Software project: https://github.com/letsencrypt/letsencrypt +Software project: https://github.com/certbot/certbot Notes for developers: https://letsencrypt.readthedocs.org/en/latest/contributing.html @@ -107,12 +107,12 @@ email to client-dev+subscribe@letsencrypt.org) -.. |build-status| image:: https://travis-ci.org/letsencrypt/letsencrypt.svg?branch=master - :target: https://travis-ci.org/letsencrypt/letsencrypt +.. |build-status| image:: https://travis-ci.org/certbot/certbot.svg?branch=master + :target: https://travis-ci.org/certbot/certbot :alt: Travis CI status -.. |coverage| image:: https://coveralls.io/repos/letsencrypt/letsencrypt/badge.svg?branch=master - :target: https://coveralls.io/r/letsencrypt/letsencrypt +.. |coverage| image:: https://coveralls.io/repos/certbot/certbot/badge.svg?branch=master + :target: https://coveralls.io/r/certbot/certbot :alt: Coverage status .. |docs| image:: https://readthedocs.org/projects/letsencrypt/badge/ @@ -159,7 +159,7 @@ Current Features - nginx/0.8.48+ (highly experimental, not included in certbot-auto) * The private key is generated locally on your system. -* Can talk to the Let's Encrypt CA or optionally to other ACME +* Can talk to the Let's Encrypt CA or optionally to other ACME compliant services. * Can get domain-validated (DV) certificates. * Can revoke certificates. @@ -174,4 +174,5 @@ Current Features .. _Freenode: https://webchat.freenode.net?channels=%23letsencrypt +.. _OFTC: https://webchat.oftc.net?channels=%23certbot .. _client-dev: https://groups.google.com/a/letsencrypt.org/forum/#!forum/client-dev From d9f36df96f458c98a81967074378118c2b7c970b Mon Sep 17 00:00:00 2001 From: Noah Swartz Date: Wed, 4 May 2016 16:02:48 -0700 Subject: [PATCH 053/396] contribute back changes from website --- docs/using.rst | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/docs/using.rst b/docs/using.rst index 0dcab4971..83377ecee 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -433,7 +433,7 @@ Operating System Packages .. code-block:: shell - sudo pacman -S letsencrypt letsencrypt-apache + sudo pacman -S letsencrypt **Debian** @@ -442,10 +442,10 @@ If you run Debian Stretch or Debian Sid, you can install letsencrypt packages. .. code-block:: shell sudo apt-get update - sudo apt-get install letsencrypt python-letsencrypt-apache + sudo apt-get install certbot python-certbot-apache If you don't want to use the Apache plugin, you can omit the -``python-letsencrypt-apache`` package. +``python-certbot-apache`` package. Packages for Debian Jessie are coming in the next few weeks. @@ -466,8 +466,12 @@ want to use the Apache plugin, it has to be installed separately: emerge -av app-crypt/letsencrypt-apache Currently, only the Apache plugin is included in Portage. However, if you -want the nginx plugin, you can use Layman to add the mrueg overlay which -does include the nginx plugin package: +Warning! +You can use Layman to add the mrueg overlay which does include a package for the +Certbot Nginx plugin, however, this plugin is known to be buggy and should only +be used with caution after creating a backup up your Nginx configuration. +We strongly recommend you use the app-crypt/letsencrypt package instead until +the Nginx plugin is ready. .. code-block:: shell From 144f28690b75e0c4deef9395d774f8b0e774c28a Mon Sep 17 00:00:00 2001 From: Noah Swartz Date: Wed, 4 May 2016 17:03:52 -0700 Subject: [PATCH 054/396] added new docs links --- CONTRIBUTING.md | 2 +- README.rst | 14 ++++++++++---- certbot-apache/docs/conf.py | 2 +- certbot-compatibility-test/docs/conf.py | 2 +- certbot-nginx/docs/conf.py | 2 +- letshelp-certbot/docs/conf.py | 2 +- 6 files changed, 15 insertions(+), 9 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bf19b18e1..5f1625658 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -15,4 +15,4 @@ to the Sphinx generated docs is provided below. --> -https://letsencrypt.readthedocs.org/en/latest/contributing.html +https://certbot.eff.org/docs/contributing.html diff --git a/README.rst b/README.rst index 60e11901c..1bfb8fdab 100644 --- a/README.rst +++ b/README.rst @@ -25,6 +25,12 @@ Until May 2016, Certbot was named simply ``letsencrypt`` or ``letsencrypt-auto`` depending on install method. Instructions on the Internet, and some pieces of the software, may still refer to this older name. +Contributing +------------ + +If you'd like to contribute to this project please read `Developer Guide +`_. + Installation ------------ @@ -46,7 +52,7 @@ Or for full command line help, type:: since ``certbot-auto`` is a wrapper to ``certbot``, it accepts exactly the same command line flags and arguments. More details about this script and other installation methods can be found `in the User Guide -`_. +`_. How to run the client --------------------- @@ -77,17 +83,17 @@ Understanding the client in more depth To understand what the client is doing in detail, it's important to understand the way it uses plugins. Please see the `explanation of -plugins `_ in +plugins `_ in the User Guide. Links ===== -Documentation: https://letsencrypt.readthedocs.org +Documentation: https://certbot.eff.org/docs Software project: https://github.com/certbot/certbot -Notes for developers: https://letsencrypt.readthedocs.org/en/latest/contributing.html +Notes for developers: https://certbot.eff.org/docs/contributing.html Main Website: https://letsencrypt.org/ diff --git a/certbot-apache/docs/conf.py b/certbot-apache/docs/conf.py index 2f996c7f4..d2fe15581 100644 --- a/certbot-apache/docs/conf.py +++ b/certbot-apache/docs/conf.py @@ -314,5 +314,5 @@ texinfo_documents = [ intersphinx_mapping = { 'python': ('https://docs.python.org/', None), 'acme': ('https://acme-python.readthedocs.org/en/latest/', None), - 'certbot': ('https://letsencrypt.readthedocs.org/en/latest/', None), + 'certbot': ('https://certbot.eff.org/docs/', None), } diff --git a/certbot-compatibility-test/docs/conf.py b/certbot-compatibility-test/docs/conf.py index 1ef69ab2d..f89f4b368 100644 --- a/certbot-compatibility-test/docs/conf.py +++ b/certbot-compatibility-test/docs/conf.py @@ -311,7 +311,7 @@ texinfo_documents = [ intersphinx_mapping = { 'python': ('https://docs.python.org/', None), 'acme': ('https://acme-python.readthedocs.org/en/latest/', None), - 'certbot': ('https://letsencrypt.readthedocs.org/en/latest/', None), + 'certbot': ('https://certbot.eff.org/docs/', None), 'certbot-apache': ( 'https://letsencrypt-apache.readthedocs.org/en/latest/', None), 'certbot-nginx': ( diff --git a/certbot-nginx/docs/conf.py b/certbot-nginx/docs/conf.py index fa00e6503..167abb4fb 100644 --- a/certbot-nginx/docs/conf.py +++ b/certbot-nginx/docs/conf.py @@ -307,5 +307,5 @@ texinfo_documents = [ intersphinx_mapping = { 'python': ('https://docs.python.org/', None), 'acme': ('https://acme-python.readthedocs.org/en/latest/', None), - 'certbot': ('https://letsencrypt.readthedocs.org/en/latest/', None), + 'certbot': ('https://certbot.eff.org/docs/', None), } diff --git a/letshelp-certbot/docs/conf.py b/letshelp-certbot/docs/conf.py index 905d70662..17d8b3ea9 100644 --- a/letshelp-certbot/docs/conf.py +++ b/letshelp-certbot/docs/conf.py @@ -307,5 +307,5 @@ texinfo_documents = [ intersphinx_mapping = { 'python': ('https://docs.python.org/', None), 'acme': ('https://acme-python.readthedocs.org/en/latest/', None), - 'certbot': ('https://letsencrypt.readthedocs.org/en/latest/', None), + 'certbot': ('https://certbot.eff.org/docs/', None), } From d38cf4a74ed565679c1cd9998c23b8af55be9bfe Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 4 May 2016 17:55:12 -0700 Subject: [PATCH 055/396] Build shim packages in next release --- tools/release.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tools/release.sh b/tools/release.sh index 06aefea44..e6169c5c8 100755 --- a/tools/release.sh +++ b/tools/release.sh @@ -45,7 +45,7 @@ export GPG_TTY=$(tty) PORT=${PORT:-1234} # subpackages to be released -SUBPKGS=${SUBPKGS:-"acme certbot-apache certbot-nginx letshelp-certbot"} +SUBPKGS=${SUBPKGS:-"acme certbot-apache certbot-nginx letshelp-certbot letsencrypt letsencrypt-apache letsencrypt-nginx letshelp-letsencrypt"} subpkgs_modules="$(echo $SUBPKGS | sed s/-/_/g)" # certbot_compatibility_test is not packaged because: # - it is not meant to be used by anyone else than Certbot devs @@ -162,12 +162,12 @@ done deactivate # pin pip hashes of the things we just built -for pkg in acme certbot certbot-apache ; do +for pkg in acme certbot certbot-apache letsencrypt letsencrypt-apache ; do echo $pkg==$version \\ pip hash dist."$version/$pkg"/*.{whl,gz} | grep "^--hash" | python2 -c 'from sys import stdin; input = stdin.read(); print " ", input.replace("\n--hash", " \\\n --hash"),' done > /tmp/hashes.$$ -if ! wc -l /tmp/hashes.$$ | grep -qE "^\s*9 " ; then +if ! wc -l /tmp/hashes.$$ | grep -qE "^\s*15 " ; then echo Unexpected pip hash output exit 1 fi From 7167a9e108c62a865a0a6d1df2084e404d3cd2ef Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Fri, 6 May 2016 16:41:23 -0700 Subject: [PATCH 056/396] fixes #2927 --- certbot/plugins/standalone.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/certbot/plugins/standalone.py b/certbot/plugins/standalone.py index a3bb1d8f0..16e9dc11b 100644 --- a/certbot/plugins/standalone.py +++ b/certbot/plugins/standalone.py @@ -120,6 +120,13 @@ def supported_challenges_validator(data): """ challs = data.split(",") + + # tls-sni-01 was dvsni during private beta + if "dvsni" in challs: + challs = [challenges.TLSSNI01.typ if chall == "dvsni" else chall + for chall in challs] + data = ",".join(challs) + unrecognized = [name for name in challs if name not in challenges.Challenge.TYPES] if unrecognized: From 56d7a97e6aeec9b91d3517f0afa4f0da815983c9 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Fri, 6 May 2016 16:44:31 -0700 Subject: [PATCH 057/396] Test dvsni with standalone-supported-challenges --- certbot/plugins/standalone_test.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/certbot/plugins/standalone_test.py b/certbot/plugins/standalone_test.py index 9f5b14591..eb6631732 100644 --- a/certbot/plugins/standalone_test.py +++ b/certbot/plugins/standalone_test.py @@ -85,6 +85,11 @@ class SupportedChallengesValidatorTest(unittest.TestCase): def test_not_subset(self): self.assertRaises(argparse.ArgumentTypeError, self._call, "dns") + def test_dvsni(self): + self.assertEqual("tls-sni-01", self._call("dvsni")) + self.assertEqual("http-01,tls-sni-01", self._call("http-01,dvsni")) + self.assertEqual("tls-sni-01,http-01", self._call("dvsni,http-01")) + class AuthenticatorTest(unittest.TestCase): """Tests for certbot.plugins.standalone.Authenticator.""" From 30ae348a8c462a770332046f31c13a8f2cb4b183 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Fri, 6 May 2016 16:53:33 -0700 Subject: [PATCH 058/396] Add logger message --- certbot/plugins/standalone.py | 1 + 1 file changed, 1 insertion(+) diff --git a/certbot/plugins/standalone.py b/certbot/plugins/standalone.py index 16e9dc11b..8e1cb72a4 100644 --- a/certbot/plugins/standalone.py +++ b/certbot/plugins/standalone.py @@ -123,6 +123,7 @@ def supported_challenges_validator(data): # tls-sni-01 was dvsni during private beta if "dvsni" in challs: + logger.info("Updating legacy standalone_supported_challenges value") challs = [challenges.TLSSNI01.typ if chall == "dvsni" else chall for chall in challs] data = ",".join(challs) From 9059a4966408fe8a495ea79539a8619728597ddc Mon Sep 17 00:00:00 2001 From: Dominic Cleal Date: Sat, 7 May 2016 18:49:18 +0100 Subject: [PATCH 059/396] Merge Augeas fix for empty section continuations From https://github.com/hercules-team/augeas/commit/568be1bc392bab6089c027de990b857ce962cc26 Fixes #2731 --- .../certbot_apache/augeas_lens/httpd.aug | 4 +- .../section-empty-continuations-2731.conf | 247 ++++++++++++++++++ 2 files changed, 249 insertions(+), 2 deletions(-) create mode 100644 certbot-apache/certbot_apache/tests/apache-conf-files/passing/section-empty-continuations-2731.conf diff --git a/certbot-apache/certbot_apache/augeas_lens/httpd.aug b/certbot-apache/certbot_apache/augeas_lens/httpd.aug index 697d5de89..07974b364 100644 --- a/certbot-apache/certbot_apache/augeas_lens/httpd.aug +++ b/certbot-apache/certbot_apache/augeas_lens/httpd.aug @@ -45,8 +45,8 @@ autoload xfm let dels (s:string) = del s s (* deal with continuation lines *) -let sep_spc = del /([ \t]+|[ \t]*\\\\\r?\n[ \t]*)/ " " -let sep_osp = del /([ \t]*|[ \t]*\\\\\r?\n[ \t]*)/ "" +let sep_spc = del /([ \t]+|[ \t]*\\\\\r?\n[ \t]*)+/ " " +let sep_osp = del /([ \t]*|[ \t]*\\\\\r?\n[ \t]*)*/ "" let sep_eq = del /[ \t]*=[ \t]*/ "=" let nmtoken = /[a-zA-Z:_][a-zA-Z0-9:_.-]*/ diff --git a/certbot-apache/certbot_apache/tests/apache-conf-files/passing/section-empty-continuations-2731.conf b/certbot-apache/certbot_apache/tests/apache-conf-files/passing/section-empty-continuations-2731.conf new file mode 100644 index 000000000..3f2f96965 --- /dev/null +++ b/certbot-apache/certbot_apache/tests/apache-conf-files/passing/section-empty-continuations-2731.conf @@ -0,0 +1,247 @@ +#ATTENTION! +# +#DO NOT MODIFY THIS FILE BECAUSE IT WAS GENERATED AUTOMATICALLY, +#SO ALL YOUR CHANGES WILL BE LOST THE NEXT TIME THE FILE IS GENERATED. + +NameVirtualHost 192.168.100.218:80 +NameVirtualHost 10.128.178.192:80 + +NameVirtualHost 192.168.100.218:443 +NameVirtualHost 10.128.178.192:443 + + +ServerName "254020-web1.example.com" +ServerAdmin "name@example.com" + +DocumentRoot "/tmp" + + + LogFormat "%h %l %u %t \"%r\" %>s %O \"%{Referer}i\" \"%{User-Agent}i\"" plesklog + + + LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"" plesklog + + + TraceEnable off + +ServerTokens ProductOnly + + + AllowOverride "All" + Options SymLinksIfOwnerMatch + Order allow,deny + Allow from all + + +php_admin_flag engine off + + + +php_admin_flag engine off + + + + + + AllowOverride All + Options SymLinksIfOwnerMatch + Order allow,deny + Allow from all + + php_admin_flag engine off + + + php_admin_flag engine off + + + + + Header add X-Powered-By PleskLin + + + + JkWorkersFile "/etc/httpd/conf/workers.properties" + JkLogFile /var/log/httpd/mod_jk.log + JkLogLevel info + + +#Include "/etc/httpd/conf/plesk.conf.d/ip_default/*.conf" + + + + ServerName "default" + UseCanonicalName Off + DocumentRoot "/tmp" + ScriptAlias /cgi-bin/ "/var/www/vhosts/default/cgi-bin" + + + + SSLEngine off + + + + AllowOverride None + Options None + Order allow,deny + Allow from all + + + + + +php_admin_flag engine on + + + +php_admin_flag engine on + + + + + + + + + + ServerName "default-192_168_100_218" + UseCanonicalName Off + DocumentRoot "/tmp" + ScriptAlias /cgi-bin/ "/var/www/vhosts/default/cgi-bin" + + + SSLEngine on + SSLVerifyClient none + #SSLCertificateFile "/usr/local/psa/var/certificates/cert-9MgutN" + + #SSLCACertificateFile "/usr/local/psa/var/certificates/cert-s6Wx3P" + + + AllowOverride None + Options None + Order allow,deny + Allow from all + + + + + +php_admin_flag engine on + + + +php_admin_flag engine on + + + + + + + ServerName "default-10_128_178_192" + UseCanonicalName Off + DocumentRoot "/tmp" + ScriptAlias /cgi-bin/ "/var/www/vhosts/default/cgi-bin" + + + SSLEngine on + SSLVerifyClient none + #SSLCertificateFile "/usr/local/psa/var/certificates/certxfb6025" + + + + AllowOverride None + Options None + Order allow,deny + Allow from all + + + + + +php_admin_flag engine on + + + +php_admin_flag engine on + + + + + + + + + + + DocumentRoot "/tmp" + ServerName lists + ServerAlias lists.* + UseCanonicalName Off + + ScriptAlias "/mailman/" "/usr/lib/mailman/cgi-bin/" + + Alias "/icons/" "/var/www/icons/" + Alias "/pipermail/" "/var/lib/mailman/archives/public/" + + + SSLEngine off + + + + + Options FollowSymLinks + Order allow,deny + Allow from all + + + + + + + DocumentRoot "/tmp" + ServerName lists + ServerAlias lists.* + UseCanonicalName Off + + ScriptAlias "/mailman/" "/usr/lib/mailman/cgi-bin/" + + Alias "/icons/" "/var/www/icons/" + Alias "/pipermail/" "/var/lib/mailman/archives/public/" + + SSLEngine on + SSLVerifyClient none + #SSLCertificateFile "/usr/local/psa/var/certificates/certxfb6025" + + + + Options FollowSymLinks + Order allow,deny + Allow from all + + + + + + + RPAFproxy_ips 192.168.100.218 10.128.178.192 + + + RPAFproxy_ips 192.168.100.218 10.128.178.192 + From 3d90fb809761deba0ffa18c615c0c5ac00887ba1 Mon Sep 17 00:00:00 2001 From: Dominic Cleal Date: Sat, 7 May 2016 21:55:31 +0100 Subject: [PATCH 060/396] Merge Augeas fix for escaped spaces in arguments From https://github.com/hercules-team/augeas/commit/f741b8b4f23dd9372dcdea18383ef9138f9c161e Fixes #2735 --- certbot-apache/certbot_apache/augeas_lens/httpd.aug | 4 ++-- .../passing/escaped-space-arguments-2735.conf | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) create mode 100644 certbot-apache/certbot_apache/tests/apache-conf-files/passing/escaped-space-arguments-2735.conf diff --git a/certbot-apache/certbot_apache/augeas_lens/httpd.aug b/certbot-apache/certbot_apache/augeas_lens/httpd.aug index 697d5de89..f27798efe 100644 --- a/certbot-apache/certbot_apache/augeas_lens/httpd.aug +++ b/certbot-apache/certbot_apache/augeas_lens/httpd.aug @@ -58,8 +58,8 @@ let empty = Util.empty_dos let indent = Util.indent (* borrowed from shellvars.aug *) -let char_arg_dir = /([^\\ '"{\t\r\n]|[^ '"{\t\r\n]+[^\\ \t\r\n])|\\\\"|\\\\'/ -let char_arg_sec = /([^\\ '"\t\r\n>]|[^ '"\t\r\n>]+[^\\ \t\r\n>])|\\\\"|\\\\'/ +let char_arg_dir = /([^\\ '"{\t\r\n]|[^ '"{\t\r\n]+[^\\ \t\r\n])|\\\\"|\\\\'|\\\\ / +let char_arg_sec = /([^\\ '"\t\r\n>]|[^ '"\t\r\n>]+[^\\ \t\r\n>])|\\\\"|\\\\'|\\\\ / let char_arg_wl = /([^\\ '"},\t\r\n]|[^ '"},\t\r\n]+[^\\ '"},\t\r\n])/ let cdot = /\\\\./ diff --git a/certbot-apache/certbot_apache/tests/apache-conf-files/passing/escaped-space-arguments-2735.conf b/certbot-apache/certbot_apache/tests/apache-conf-files/passing/escaped-space-arguments-2735.conf new file mode 100644 index 000000000..1ea53dfab --- /dev/null +++ b/certbot-apache/certbot_apache/tests/apache-conf-files/passing/escaped-space-arguments-2735.conf @@ -0,0 +1,2 @@ +RewriteCond %{HTTP:Content-Disposition} \.php [NC] +RewriteCond %{THE_REQUEST} ^[A-Z]{3,9}\ /.+/trackback/?\ HTTP/ [NC] From 653c7b6327508a3dba2d8a2ef7c5c4463647d7fe Mon Sep 17 00:00:00 2001 From: Michal Moravec Date: Sun, 8 May 2016 16:16:54 +0200 Subject: [PATCH 061/396] Ensure /usr/local/lib/ exists before creating libaugeas.dylib symlink in mac.sh bootstraper --- letsencrypt-auto-source/pieces/bootstrappers/mac.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/letsencrypt-auto-source/pieces/bootstrappers/mac.sh b/letsencrypt-auto-source/pieces/bootstrappers/mac.sh index 79e58eb3f..e41db04b1 100755 --- a/letsencrypt-auto-source/pieces/bootstrappers/mac.sh +++ b/letsencrypt-auto-source/pieces/bootstrappers/mac.sh @@ -26,7 +26,8 @@ BootstrapMac() { # Workaround for _dlopen not finding augeas on OS X if [ "$pkgman" = "port" ] && ! [ -e "/usr/local/lib/libaugeas.dylib" ] && [ -e "/opt/local/lib/libaugeas.dylib" ]; then echo "Applying augeas workaround" - $SUDO ln -s /opt/local/lib/libaugeas.dylib /usr/local/lib + $SUDO mkdir -p /usr/local/lib/ + $SUDO ln -s /opt/local/lib/libaugeas.dylib /usr/local/lib/ fi if ! hash pip 2>/dev/null; then From 3c413c28b8e6ce0208289ccd12fdc8252c1ce805 Mon Sep 17 00:00:00 2001 From: Noah Swartz Date: Mon, 9 May 2016 12:31:39 -0700 Subject: [PATCH 062/396] fix grammar --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 1bfb8fdab..a0bd4059b 100644 --- a/README.rst +++ b/README.rst @@ -3,7 +3,7 @@ Disclaimer ========== -The Certbot is **BETA SOFTWARE**. It contains plenty of bugs and +Certbot is **BETA SOFTWARE**. It contains plenty of bugs and rough edges, and should be tested thoroughly in staging environments before use on production systems. From 555513940c8e541923cb2e28a56d62b684559dd3 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 9 May 2016 14:57:01 -0700 Subject: [PATCH 063/396] Explain *-hook and -q in renewal documentation --- docs/using.rst | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/docs/using.rst b/docs/using.rst index 60c074d75..7d7b4fbfd 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -215,12 +215,25 @@ expire in less than 30 days. The same plugin and options that were used at the time the certificate was originally issued will be used for the renewal attempt, unless you specify other plugins or options. +You can also specify hooks to be run before or after a certificate is +renewed. For example, if you want to use the standalone_ plugin to renew +your certificates, you may want to use a command like + +``letsencrypt renew --standalone --pre-hook "service nginx stop" --post-hook "service nginx start"`` + +This will stop Nginx so standalone can bind to the necessary ports and +then restart Nginx after the plugin is finished. The hooks will only be +run if a certificate is due for renewal, so you can run this command +frequently without unnecessarily stopping your webserver. More +information about renewal hooks can be found by running +``letsencrypt --help renew``. + If you're sure that this command executes successfully without human intervention, you can add the command to ``crontab`` (since certificates are only renewed when they're determined to be near expiry, the command -can run on a regular basis, like every week or every day); note that -the current version provides detailed output describing either renewal -success or failure. +can run on a regular basis, like every week or every day). You may also +want to use the ``-q`` or ``--quiet`` quiet flag to silence all output +except errors. The ``--force-renew`` flag may be helpful for automating renewal; it causes the expiration time of the certificate(s) to be ignored when From 9ddabc3e9a2fcc89dd7d2f892e6a726cdb070325 Mon Sep 17 00:00:00 2001 From: Noah Swartz Date: Mon, 9 May 2016 15:17:14 -0700 Subject: [PATCH 064/396] fix docs conf --- docs/conf.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index fb2bdea73..b2b31ac5d 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -64,8 +64,8 @@ source_suffix = '.rst' master_doc = 'index' # General information about the project. -project = u'Let\'s Encrypt' -copyright = u'2014-2015, Let\'s Encrypt Project' +project = u'Certbot' +copyright = u'2014-2016, Certbot Project' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the @@ -225,7 +225,7 @@ html_static_path = ['_static'] #html_search_scorer = 'scorer.js' # Output file base name for HTML help builder. -htmlhelp_basename = 'LetsEncryptdoc' +htmlhelp_basename = 'Certbotdoc' # -- Options for LaTeX output --------------------------------------------- @@ -247,8 +247,8 @@ latex_elements = { # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ - ('index', 'LetsEncrypt.tex', u'Let\'s Encrypt Documentation', - u'Let\'s Encrypt Project', 'manual'), + ('index', 'Certbot.tex', u'Certbot Documentation', + u'Certbot Project', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of @@ -277,7 +277,7 @@ latex_documents = [ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ - ('index', 'certbot', u'Let\'s Encrypt Documentation', + ('index', 'certbot', u'Certbot Documentation', [project], 7), ('man/certbot', 'certbot', u'certbot script documentation', [project], 1), @@ -293,8 +293,8 @@ man_pages = [ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - ('index', 'LetsEncrypt', u'Let\'s Encrypt Documentation', - u'Let\'s Encrypt Project', 'LetsEncrypt', 'One line description of project.', + ('index', 'Certbot', u'Certbot Documentation', + u'Certbot Project', 'Certbot', 'One line description of project.', 'Miscellaneous'), ] From bbcde8cec1096ef44825862a66546d565d7419d6 Mon Sep 17 00:00:00 2001 From: Noah Swartz Date: Mon, 9 May 2016 15:27:17 -0700 Subject: [PATCH 065/396] seth changes --- docs/ciphers.rst | 4 ++-- docs/using.rst | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/ciphers.rst b/docs/ciphers.rst index 5a046e757..8996dd9ef 100644 --- a/docs/ciphers.rst +++ b/docs/ciphers.rst @@ -131,7 +131,7 @@ The Let's Encrypt project may deviate from the Mozilla recommendations in the future if good cause is shown and we believe our users' priorities would be well-served by doing so. In general, please address relevant proposals for changing priorities to the Mozilla security -team first, before asking the Let's Encrypt project or EFF to change +team first, before asking the Certbot developers to change Certbot's priorities. The Mozilla security team is likely to have more resources and expertise to bring to bear on evaluating reasons why its recommendations should be updated. @@ -187,7 +187,7 @@ to enable updating ciphers with each new Certbot release, or certbot --update-ciphers off to disable automatic configuration updates. These features have not yet -been implemented and this syntax may change then they are implemented. +been implemented and this syntax may change when they are implemented. TODO diff --git a/docs/using.rst b/docs/using.rst index 10d4aa544..f215b335b 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -539,7 +539,7 @@ Comparison of different methods Unless you have a very specific requirements, we kindly suggest that you use the certbot-auto_ method. It's the fastest, the most thoroughly tested and the most reliable way of getting our software and the free -SSL certificates! +TLS/SSL certificates! Beyond the methods discussed here, other methods may be possible, such as installing Certbot directly with pip from PyPI or downloading a ZIP From 6fa7eb576bc5042b8455e1da85c60ee858253dac Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 9 May 2016 18:11:36 -0700 Subject: [PATCH 066/396] Use -n when using certonly --- docs/using.rst | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/using.rst b/docs/using.rst index 7d7b4fbfd..748fae370 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -254,9 +254,11 @@ renewals of that certificate. An alternative form that provides for more fine-grained control over the renewal process (while renewing specified certificates one at a time), is ``letsencrypt certonly`` with the complete set of subject domains of -a specific certificate specified via `-d` flags, like +a specific certificate specified via `-d` flags. You may also want to +include the ``-n`` or ``--noninteractive`` flag to prevent blocking on +user input (which is useful when running the command from cron). -``letsencrypt certonly -d example.com -d www.example.com`` +``letsencrypt certonly -n -d example.com -d www.example.com`` (All of the domains covered by the certificate must be specified in this case in order to renew and replace the old certificate rather From 3ce47282ea37f2c9b87868ae731c0dcacbef0e12 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 9 May 2016 18:14:02 -0700 Subject: [PATCH 067/396] Make --quiet suggestion stronger --- docs/using.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/using.rst b/docs/using.rst index 748fae370..e7e0e9474 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -231,9 +231,9 @@ information about renewal hooks can be found by running If you're sure that this command executes successfully without human intervention, you can add the command to ``crontab`` (since certificates are only renewed when they're determined to be near expiry, the command -can run on a regular basis, like every week or every day). You may also -want to use the ``-q`` or ``--quiet`` quiet flag to silence all output -except errors. +can run on a regular basis, like every week or every day). In that case, +you are likely to want to use the ``-q`` or ``--quiet`` quiet flag to +silence all output except errors. The ``--force-renew`` flag may be helpful for automating renewal; it causes the expiration time of the certificate(s) to be ignored when From 7f8fadee37e6c2de78786338d9a30c8a21e9995f Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 9 May 2016 18:33:47 -0700 Subject: [PATCH 068/396] Revert "Make certbot accept --yes flag" This reverts commit a9ecd146a9797eb068827bf6c357fde15f3a9a03. --- certbot/cli.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/certbot/cli.py b/certbot/cli.py index 40af5dccb..e2c57595b 100644 --- a/certbot/cli.py +++ b/certbot/cli.py @@ -649,9 +649,6 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): "automation", "--no-self-upgrade", action="store_true", help="(letsencrypt-auto only) prevent the letsencrypt-auto script from" " upgrading itself to newer released versions") - helpful.add( - "automation", "--yes", action="store_true", - help="(letsencrypt-auto only) assume yes is the answer to all prompts") helpful.add( "automation", "-q", "--quiet", dest="quiet", action="store_true", help="Silence all output except errors. Useful for automation via cron." From f38d59d6756f76be4f51b86e1365556da569216f Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 9 May 2016 19:08:25 -0700 Subject: [PATCH 069/396] Use --non-interactive instead of --yes and use getopt for parsing short opts --- letsencrypt-auto-source/letsencrypt-auto | 31 +++++++++++-------- .../letsencrypt-auto.template | 31 +++++++++++-------- 2 files changed, 36 insertions(+), 26 deletions(-) diff --git a/letsencrypt-auto-source/letsencrypt-auto b/letsencrypt-auto-source/letsencrypt-auto index adeb94e3e..538226d2f 100755 --- a/letsencrypt-auto-source/letsencrypt-auto +++ b/letsencrypt-auto-source/letsencrypt-auto @@ -29,15 +29,26 @@ all arguments you have provided. Help for certbot itself cannot be provided until it is installed. - --debug attempt installation on experimental platforms - --help print this help - --no-self-upgrade do not download updates for certbot or certbot-auto - --os-packages-only install OS dependencies and exit - -v, --verbose provide more output - --yes assume yes is the answer to all prompts + --debug attempt experimental installation + -h, --help print this help + -n, --non-interactive, --noninteractive run without asking for user input + --no-self-upgrade do not download updates + --os-packages-only install OS dependencies and exit + -v, --verbose provide more output All arguments are accepted and forwarded to the Certbot client when run." +while getopts ":hnv" arg; do + case $arg in + h) + HELP=1;; + n) + ASSUME_YES=1;; + v) + VERBOSE=1;; + esac +done + for arg in "$@" ; do case "$arg" in --debug) @@ -50,16 +61,10 @@ for arg in "$@" ; do NO_SELF_UPGRADE=1;; --help) HELP=1;; - --yes) + --noninteractive|--non-interactive) ASSUME_YES=1;; --verbose) VERBOSE=1;; - [!-]*|-*[!v]*|-) - # Anything that isn't -v, -vv, etc.: that is, anything that does not - # start with a -, contains anything that's not a v, or is just "-" - ;; - *) # -v+ remains. - VERBOSE=1;; esac done diff --git a/letsencrypt-auto-source/letsencrypt-auto.template b/letsencrypt-auto-source/letsencrypt-auto.template index fe4baeafd..af4ed62d3 100755 --- a/letsencrypt-auto-source/letsencrypt-auto.template +++ b/letsencrypt-auto-source/letsencrypt-auto.template @@ -29,15 +29,26 @@ all arguments you have provided. Help for certbot itself cannot be provided until it is installed. - --debug attempt installation on experimental platforms - --help print this help - --no-self-upgrade do not download updates for certbot or certbot-auto - --os-packages-only install OS dependencies and exit - -v, --verbose provide more output - --yes assume yes is the answer to all prompts + --debug attempt experimental installation + -h, --help print this help + -n, --non-interactive, --noninteractive run without asking for user input + --no-self-upgrade do not download updates + --os-packages-only install OS dependencies and exit + -v, --verbose provide more output All arguments are accepted and forwarded to the Certbot client when run." +while getopts ":hnv" arg; do + case $arg in + h) + HELP=1;; + n) + ASSUME_YES=1;; + v) + VERBOSE=1;; + esac +done + for arg in "$@" ; do case "$arg" in --debug) @@ -50,16 +61,10 @@ for arg in "$@" ; do NO_SELF_UPGRADE=1;; --help) HELP=1;; - --yes) + --noninteractive|--non-interactive) ASSUME_YES=1;; --verbose) VERBOSE=1;; - [!-]*|-*[!v]*|-) - # Anything that isn't -v, -vv, etc.: that is, anything that does not - # start with a -, contains anything that's not a v, or is just "-" - ;; - *) # -v+ remains. - VERBOSE=1;; esac done From 8394e5eb64e4cd4b18b6432340bc335349a8bec6 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Mon, 9 May 2016 19:07:11 -0700 Subject: [PATCH 070/396] Draft of new installation instructions --- README.rst | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index 91a3cfcb5..3ba541f77 100644 --- a/README.rst +++ b/README.rst @@ -30,9 +30,15 @@ systems have packages yet, we provide a temporary solution via the ``letsencrypt-auto`` wrapper script, which obtains some dependencies from your OS and puts others in a python virtual environment:: - user@webserver:~$ git clone https://github.com/letsencrypt/letsencrypt - user@webserver:~$ cd letsencrypt - user@webserver:~/letsencrypt$ ./letsencrypt-auto --help + user@webserver:~$ wget https://dl.eff.org/certbot-auto + user@webserver:~$ chmod a+x ./certbot-auto + user@webserver:~$ ./certbot-auto --help + +.. hint:: For stronger security, you can use these steps for extra verification before running the script: + + user@webserver:~$ wget https://dl.eff.org/certbot-auto.sig + user@webserver:~$ gpg2 --recv-key A2CFB51FA275A7286234E7B24D17C995CD9775F2 + user@webserver:~$ gpg2 --trusted-key 4D17C995CD9775F2 --verify certbot-auto.sig certbot-auto Or for full command line help, type:: From 675f2e5413f5e13b8062351ca94f2bfd610498fe Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Mon, 9 May 2016 19:24:47 -0700 Subject: [PATCH 071/396] Put the signature instructions in a hint box --- README.rst | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/README.rst b/README.rst index 3ba541f77..040aa6bdf 100644 --- a/README.rst +++ b/README.rst @@ -34,13 +34,14 @@ from your OS and puts others in a python virtual environment:: user@webserver:~$ chmod a+x ./certbot-auto user@webserver:~$ ./certbot-auto --help -.. hint:: For stronger security, you can use these steps for extra verification before running the script: +.. hint:: If you'd like stronger security when downloading the ``certbot-auto`` script, + you can use these steps for extra verification before running it:: - user@webserver:~$ wget https://dl.eff.org/certbot-auto.sig - user@webserver:~$ gpg2 --recv-key A2CFB51FA275A7286234E7B24D17C995CD9775F2 - user@webserver:~$ gpg2 --trusted-key 4D17C995CD9775F2 --verify certbot-auto.sig certbot-auto + user@server:~$ wget https://dl.eff.org/certbot-auto.sig + user@server:~$ gpg2 --recv-key A2CFB51FA275A7286234E7B24D17C995CD9775F2 + user@server:~$ gpg2 --trusted-key 4D17C995CD9775F2 --verify certbot-auto.sig certbot-auto -Or for full command line help, type:: +And for full command line help, you can type:: ./letsencrypt-auto --help all From 37efe306754bf8a1f111deb0933a2597ec0a4024 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Mon, 9 May 2016 19:31:29 -0700 Subject: [PATCH 072/396] Better explanation --- README.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 040aa6bdf..840cfff0a 100644 --- a/README.rst +++ b/README.rst @@ -34,8 +34,8 @@ from your OS and puts others in a python virtual environment:: user@webserver:~$ chmod a+x ./certbot-auto user@webserver:~$ ./certbot-auto --help -.. hint:: If you'd like stronger security when downloading the ``certbot-auto`` script, - you can use these steps for extra verification before running it:: +.. hint:: The certbot-auto download is protected by HTTPS, which is pretty good, but if you'd like to + double check the integrity of the ``certbot-auto`` script, you can use these steps for verification before running it:: user@server:~$ wget https://dl.eff.org/certbot-auto.sig user@server:~$ gpg2 --recv-key A2CFB51FA275A7286234E7B24D17C995CD9775F2 From 7a848d2b048b07c1b2fbd5f81b87c2a6cda46359 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 9 May 2016 19:51:08 -0700 Subject: [PATCH 073/396] Remove unneeded info about backports --- letsencrypt-auto-source/letsencrypt-auto | 3 --- letsencrypt-auto-source/pieces/bootstrappers/deb_common.sh | 3 --- 2 files changed, 6 deletions(-) diff --git a/letsencrypt-auto-source/letsencrypt-auto b/letsencrypt-auto-source/letsencrypt-auto index 538226d2f..8dbdf5f9c 100755 --- a/letsencrypt-auto-source/letsencrypt-auto +++ b/letsencrypt-auto-source/letsencrypt-auto @@ -192,9 +192,6 @@ BootstrapDebCommon() { if ! grep -v -e ' *#' /etc/apt/sources.list | grep -q "$BACKPORT_NAME" ; then # This can theoretically error if sources.list.d is empty, but in that case we don't care. if ! grep -v -e ' *#' /etc/apt/sources.list.d/* 2>/dev/null | grep -q "$BACKPORT_NAME"; then - if echo $BACKPORT_NAME | grep -q wheezy ; then - echo 'Backports are only installed if explicitly requested via "apt-get install -t wheezy-backports"' - fi if [ "$ASSUME_YES" = 1 ]; then /bin/echo -n "Installing augeas from $BACKPORT_NAME in 3 seconds..." sleep 1s diff --git a/letsencrypt-auto-source/pieces/bootstrappers/deb_common.sh b/letsencrypt-auto-source/pieces/bootstrappers/deb_common.sh index e91f52261..bfbcfa31d 100644 --- a/letsencrypt-auto-source/pieces/bootstrappers/deb_common.sh +++ b/letsencrypt-auto-source/pieces/bootstrappers/deb_common.sh @@ -46,9 +46,6 @@ BootstrapDebCommon() { if ! grep -v -e ' *#' /etc/apt/sources.list | grep -q "$BACKPORT_NAME" ; then # This can theoretically error if sources.list.d is empty, but in that case we don't care. if ! grep -v -e ' *#' /etc/apt/sources.list.d/* 2>/dev/null | grep -q "$BACKPORT_NAME"; then - if echo $BACKPORT_NAME | grep -q wheezy ; then - echo 'Backports are only installed if explicitly requested via "apt-get install -t wheezy-backports"' - fi if [ "$ASSUME_YES" = 1 ]; then /bin/echo -n "Installing augeas from $BACKPORT_NAME in 3 seconds..." sleep 1s From 86cb5b68e3b2f3148a7d4134ffd786a70c34ee58 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Tue, 10 May 2016 10:14:03 -0700 Subject: [PATCH 074/396] cli_command should sometimes be certbot-auto --- certbot/cli.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/certbot/cli.py b/certbot/cli.py index 7ed8a0d3a..90e86a751 100644 --- a/certbot/cli.py +++ b/certbot/cli.py @@ -38,6 +38,11 @@ helpful_parser = None # fails safely LEAUTO = "letsencrypt-auto" +if "CERTBOT_AUTO" in os.environ: + # if we're here, this is probably going to be certbot-auto, unless the + # user saved the script under a different name + LEAUTO = os.path.basename(os.environ["CERTBOT_AUTO"]) + fragment = os.path.join(".local", "share", "letsencrypt") cli_command = LEAUTO if fragment in sys.argv[0] else "certbot" From ed23f2e27fb42aac0b37cc9df74e6853a912e75a Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Tue, 10 May 2016 10:21:15 -0700 Subject: [PATCH 075/396] CERTBOT_AUTO env was broken (especially if containing spaces) --- letsencrypt-auto-source/letsencrypt-auto | 14 +++++++++++--- letsencrypt-auto-source/letsencrypt-auto.template | 14 +++++++++++--- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/letsencrypt-auto-source/letsencrypt-auto b/letsencrypt-auto-source/letsencrypt-auto index 1f2dfcf85..72adfccb1 100755 --- a/letsencrypt-auto-source/letsencrypt-auto +++ b/letsencrypt-auto-source/letsencrypt-auto @@ -55,7 +55,7 @@ export CERTBOT_AUTO="$0" if test "`id -u`" -ne "0" ; then if command -v sudo 1>/dev/null 2>&1; then SUDO=sudo - SUDO_ENV="CERTBOT_AUTO=\"$0\"" + SUDO_ENV="CERTBOT_AUTO=$0" else echo \"sudo\" is not available, will use \"su\" for installation steps... # Because the parameters in `su -c` has to be a string, @@ -827,8 +827,16 @@ UNLIKELY_EOF echo "Installation succeeded." fi echo "Requesting root privileges to run certbot..." - echo " " $SUDO $SUDO_ENV "$VENV_BIN/letsencrypt" "$@" - $SUDO $SUDO_ENV "$VENV_BIN/letsencrypt" "$@" + if [ -z "$SUDO_ENV" ] ; then + # SUDO is su wrapper / noop + echo " " $SUDO "$VENV_BIN/letsencrypt" "$@" + $SUDO "$VENV_BIN/letsencrypt" "$@" + else + # sudo + echo " " $SUDO "$SUDO_ENV" "$VENV_BIN/letsencrypt" "$@" + $SUDO "$SUDO_ENV" "$VENV_BIN/letsencrypt" "$@" + fi + else # Phase 1: Upgrade letsencrypt-auto if neceesary, then self-invoke. # diff --git a/letsencrypt-auto-source/letsencrypt-auto.template b/letsencrypt-auto-source/letsencrypt-auto.template index 580ee7c07..4e32d8b4f 100755 --- a/letsencrypt-auto-source/letsencrypt-auto.template +++ b/letsencrypt-auto-source/letsencrypt-auto.template @@ -55,7 +55,7 @@ export CERTBOT_AUTO="$0" if test "`id -u`" -ne "0" ; then if command -v sudo 1>/dev/null 2>&1; then SUDO=sudo - SUDO_ENV="CERTBOT_AUTO=\"$0\"" + SUDO_ENV="CERTBOT_AUTO=$0" else echo \"sudo\" is not available, will use \"su\" for installation steps... # Because the parameters in `su -c` has to be a string, @@ -223,8 +223,16 @@ UNLIKELY_EOF echo "Installation succeeded." fi echo "Requesting root privileges to run certbot..." - echo " " $SUDO $SUDO_ENV "$VENV_BIN/letsencrypt" "$@" - $SUDO $SUDO_ENV "$VENV_BIN/letsencrypt" "$@" + if [ -z "$SUDO_ENV" ] ; then + # SUDO is su wrapper / noop + echo " " $SUDO "$VENV_BIN/letsencrypt" "$@" + $SUDO "$VENV_BIN/letsencrypt" "$@" + else + # sudo + echo " " $SUDO "$SUDO_ENV" "$VENV_BIN/letsencrypt" "$@" + $SUDO "$SUDO_ENV" "$VENV_BIN/letsencrypt" "$@" + fi + else # Phase 1: Upgrade letsencrypt-auto if neceesary, then self-invoke. # From 62cf9c93a8dbaad2bb0523828c5072ed8288ca32 Mon Sep 17 00:00:00 2001 From: "Gregory L. Dietsche" Date: Tue, 10 May 2016 01:40:44 +0000 Subject: [PATCH 076/396] /etc/issue does not exist on all systems Signed-off-by: Gregory L. Dietsche --- letsencrypt-auto-source/letsencrypt-auto.template | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/letsencrypt-auto-source/letsencrypt-auto.template b/letsencrypt-auto-source/letsencrypt-auto.template index 2c8e1ec4c..c451340bc 100755 --- a/letsencrypt-auto-source/letsencrypt-auto.template +++ b/letsencrypt-auto-source/letsencrypt-auto.template @@ -154,7 +154,7 @@ Bootstrap() { ExperimentalBootstrap "FreeBSD" BootstrapFreeBsd elif uname | grep -iq Darwin ; then ExperimentalBootstrap "Mac OS X" BootstrapMac - elif grep -iq "Amazon Linux" /etc/issue ; then + elif [ -f /etc/issue ] && grep -iq "Amazon Linux" /etc/issue ; then ExperimentalBootstrap "Amazon Linux" BootstrapRpmCommon else echo "Sorry, I don't know how to bootstrap Certbot on your operating system!" From 029a818370a013a0b9ec623d5065f5eac86249e7 Mon Sep 17 00:00:00 2001 From: "Gregory L. Dietsche" Date: Tue, 10 May 2016 01:44:51 +0000 Subject: [PATCH 077/396] Experimental Joyent SmartOS Support Testing using image: 088b97b0-e1a1-11e5-b895-9baa2086eb33 base-64-lts 15.4.1 Signed-off-by: Gregory L. Dietsche --- letsencrypt-auto-source/letsencrypt-auto.template | 3 +++ letsencrypt-auto-source/pieces/bootstrappers/smartos.sh | 4 ++++ 2 files changed, 7 insertions(+) create mode 100644 letsencrypt-auto-source/pieces/bootstrappers/smartos.sh diff --git a/letsencrypt-auto-source/letsencrypt-auto.template b/letsencrypt-auto-source/letsencrypt-auto.template index c451340bc..1a66753f1 100755 --- a/letsencrypt-auto-source/letsencrypt-auto.template +++ b/letsencrypt-auto-source/letsencrypt-auto.template @@ -122,6 +122,7 @@ DeterminePythonVersion() { {{ bootstrappers/gentoo_common.sh }} {{ bootstrappers/free_bsd.sh }} {{ bootstrappers/mac.sh }} +{{ bootstrappers/smartos.sh }} # Install required OS packages: Bootstrap() { @@ -156,6 +157,8 @@ Bootstrap() { ExperimentalBootstrap "Mac OS X" BootstrapMac elif [ -f /etc/issue ] && grep -iq "Amazon Linux" /etc/issue ; then ExperimentalBootstrap "Amazon Linux" BootstrapRpmCommon + elif [ -f /etc/product ] && grep -q "Joyent Instance" /etc/product ; then + ExperimentalBootstrap "Joyent SmartOS Zone" BootstrapSmartOS else echo "Sorry, I don't know how to bootstrap Certbot on your operating system!" echo diff --git a/letsencrypt-auto-source/pieces/bootstrappers/smartos.sh b/letsencrypt-auto-source/pieces/bootstrappers/smartos.sh new file mode 100644 index 000000000..e721c1c0b --- /dev/null +++ b/letsencrypt-auto-source/pieces/bootstrappers/smartos.sh @@ -0,0 +1,4 @@ +BootstrapSmartOS() { + pkgin update + pkgin -y install 'gcc49' 'py27-augeas' 'py27-virtualenv' +} From 762e9e9db0b2b2eb6711b19b016da740946a63a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcel=20Kr=C3=BCger?= Date: Wed, 11 May 2016 06:16:13 +0200 Subject: [PATCH 078/396] Fix mime.types parsing for nginx Added characters to key parsing rule which appear in the mime type in mime.types. --- certbot-nginx/certbot_nginx/nginxparser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certbot-nginx/certbot_nginx/nginxparser.py b/certbot-nginx/certbot_nginx/nginxparser.py index cef0756d7..8ab7adb0a 100644 --- a/certbot-nginx/certbot_nginx/nginxparser.py +++ b/certbot-nginx/certbot_nginx/nginxparser.py @@ -17,7 +17,7 @@ class RawNginxParser(object): right_bracket = Literal("}").suppress() semicolon = Literal(";").suppress() space = White().suppress() - key = Word(alphanums + "_/") + key = Word(alphanums + "_/+-.") # Matches anything that is not a special character AND any chars in single # or double quotes value = Regex(r"((\".*\")?(\'.*\')?[^\{\};,]?)+") From 639efaeb7ba590ba056c60fdcccb7c0a5b77e69a Mon Sep 17 00:00:00 2001 From: chrismarget Date: Wed, 11 May 2016 12:01:53 -0400 Subject: [PATCH 079/396] Randomize serial numbers of DVSNI challenge certificates. --- acme/acme/crypto_util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/acme/acme/crypto_util.py b/acme/acme/crypto_util.py index 73f7f8f62..e121b1ac3 100644 --- a/acme/acme/crypto_util.py +++ b/acme/acme/crypto_util.py @@ -203,7 +203,7 @@ def gen_ss_cert(key, domains, not_before=None, """ assert domains, "Must provide one or more hostnames for the cert." cert = OpenSSL.crypto.X509() - cert.set_serial_number(1337) + cert.set_serial_number(int(OpenSSL.rand.bytes(16).encode("hex"), 16)) cert.set_version(2) extensions = [ From 22f6b77e680af0144b260a85ff51c53a34689179 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Tue, 10 May 2016 18:51:16 -0700 Subject: [PATCH 080/396] Move deprecation warning after logging setup --- certbot/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certbot/main.py b/certbot/main.py index 939a5e0e4..0405d6eb5 100644 --- a/certbot/main.py +++ b/certbot/main.py @@ -649,7 +649,6 @@ def main(cli_args=sys.argv[1:]): args = cli.prepare_and_parse_args(plugins, cli_args) config = configuration.NamespaceConfig(args) zope.component.provideUtility(config) - cli.possible_deprecation_warning(config) # Setup logging ASAP, otherwise "No handlers could be found for # logger ..." TODO: this should be done before plugins discovery @@ -662,6 +661,7 @@ def main(cli_args=sys.argv[1:]): le_util.make_or_verify_dir( config.logs_dir, 0o700, os.geteuid(), "--strict-permissions" in cli_args) setup_logging(config, _cli_log_handler, logfile='letsencrypt.log') + cli.possible_deprecation_warning(config) logger.debug("certbot version: %s", certbot.__version__) # do not log `config`, as it contains sensitive data (e.g. revoke --key)! From 9556dfac5a00114f2dc09f1a86f4fd5f127626a6 Mon Sep 17 00:00:00 2001 From: Seth Schoen Date: Wed, 11 May 2016 10:44:40 -0700 Subject: [PATCH 081/396] Add a way to update registrations Fixes #1215 --- certbot/cli.py | 10 ++++++++-- certbot/main.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/certbot/cli.py b/certbot/cli.py index 97b1a5399..b4460f681 100644 --- a/certbot/cli.py +++ b/certbot/cli.py @@ -265,8 +265,9 @@ class HelpfulArgumentParser(object): self.VERBS = {"auth": main.obtain_cert, "certonly": main.obtain_cert, "config_changes": main.config_changes, "run": main.run, "install": main.install, "plugins": main.plugins_cmd, - "renew": main.renew, "revoke": main.revoke, - "rollback": main.rollback, "everything": main.run} + "register": main.register, "renew": main.renew, + "revoke": main.revoke, "rollback": main.rollback, + "everything": main.run} # List of topics for which additional help can be provided HELP_TOPICS = ["all", "security", "paths", "automation", "testing"] + list(self.VERBS) @@ -591,6 +592,11 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): "certificates. Updates to the Subscriber Agreement will still " "affect you, and will be effective 14 days after posting an " "update to the web site.") + helpful.add( + None, "--update-registration", action="store_true", + help="With the register verb, indicates that details associated " + "with an existing registration, such as the e-mail address, " + "should be updated, rather than registering a new account.") 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/certbot/main.py b/certbot/main.py index 309889e8e..08a5a3ea4 100644 --- a/certbot/main.py +++ b/certbot/main.py @@ -10,6 +10,7 @@ import traceback import zope.component +from acme import errors as acme_errors from acme import jose import certbot @@ -363,6 +364,33 @@ def _init_le_client(config, authenticator, installer): return client.Client(config, acc, authenticator, installer, acme=acme) +def register(config, unused_plugins): + """Create or modify accounts on the server.""" + + # Currently, only --update-registration is implemented. Issue #2446 + # calls for a fuller register verb, to allow better separation of + # account management from obtaining certs. + if not config.update_registration: + return "Currently, only register --update-registration is implemented." + if config.email is None: + return ("Currently, --update-registration can only change the e-mail " + "address\nassociated with an account. A new e-mail address is " + "required\n(hint: --email)") + acc, acme = _determine_account(config) + acme_client = client.Client(config, acc, None, None, acme=acme) + try: + updated_reg = client.messages.Registration.from_data(email=config.email) + acme_client.acme.update_registration(acme_client.account.regr, + updated_reg) + except acme_errors.UnexpectedUpdate: + # We expect the unexpected update! + pass + query_data = acme_client.acme.query_registration(acme_client.account.regr) + # We rely on an ACME exception to interrupt this process if it didn't work. + print("Registration change succeeded. New registration data:\n") + print(query_data) + + def install(config, plugins): """Install a previously obtained cert in a server.""" # XXX: Update for renewer/RenewableCert From a7ef4940b6fabde1730c2a4cf7aef2d689895dff Mon Sep 17 00:00:00 2001 From: chrismarget Date: Wed, 11 May 2016 13:57:18 -0400 Subject: [PATCH 082/396] Randomize DVSNI challenge certificate serial number, now for python 3.3. --- acme/acme/crypto_util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/acme/acme/crypto_util.py b/acme/acme/crypto_util.py index e121b1ac3..6cb5ad6fc 100644 --- a/acme/acme/crypto_util.py +++ b/acme/acme/crypto_util.py @@ -203,7 +203,7 @@ def gen_ss_cert(key, domains, not_before=None, """ assert domains, "Must provide one or more hostnames for the cert." cert = OpenSSL.crypto.X509() - cert.set_serial_number(int(OpenSSL.rand.bytes(16).encode("hex"), 16)) + cert.set_serial_number(int.from_bytes(OpenSSL.rand.bytes(16),'big')) cert.set_version(2) extensions = [ From 6f9e28fccad96c19b3fd58560d5c6ddba8201d04 Mon Sep 17 00:00:00 2001 From: Jacob Hoffman-Andrews Date: Wed, 11 May 2016 09:49:23 -0700 Subject: [PATCH 083/396] Allow unrecognized fields in directory. --- acme/acme/messages.py | 11 +---------- acme/acme/messages_test.py | 9 ++++----- 2 files changed, 5 insertions(+), 15 deletions(-) diff --git a/acme/acme/messages.py b/acme/acme/messages.py index 24a3b580c..4a9bd9f47 100644 --- a/acme/acme/messages.py +++ b/acme/acme/messages.py @@ -143,12 +143,6 @@ class Directory(jose.JSONDeSerializable): def __init__(self, jobj): canon_jobj = util.map_keys(jobj, self._canon_key) - if not set(canon_jobj).issubset( - set(self._REGISTERED_TYPES).union(['meta'])): - # TODO: acme-spec is not clear about this: 'It is a JSON - # dictionary, whose keys are the "resource" values listed - # in {{https-requests}}' - raise ValueError('Wrong directory fields') # TODO: check that everything is an absolute URL; acme-spec is # not clear on that self._jobj = canon_jobj @@ -171,10 +165,7 @@ class Directory(jose.JSONDeSerializable): @classmethod def from_json(cls, jobj): jobj['meta'] = cls.Meta.from_json(jobj.pop('meta', {})) - try: - return cls(jobj) - except ValueError as error: - raise jose.DeserializationError(str(error)) + return cls(jobj) class Resource(jose.JSONObjectWithFields): diff --git a/acme/acme/messages_test.py b/acme/acme/messages_test.py index b2b7febdc..b39b9ff55 100644 --- a/acme/acme/messages_test.py +++ b/acme/acme/messages_test.py @@ -97,9 +97,9 @@ class DirectoryTest(unittest.TestCase): ), }) - def test_init_wrong_key_value_error(self): + def test_init_wrong_key_value_success(self): # pylint: disable=no-self-use from acme.messages import Directory - self.assertRaises(ValueError, Directory, {'foo': 'bar'}) + Directory({'foo': 'bar'}) def test_getitem(self): self.assertEqual('reg', self.dir['new-reg']) @@ -127,10 +127,9 @@ class DirectoryTest(unittest.TestCase): }, }) - def test_from_json_deserialization_error_on_wrong_key(self): + def test_from_json_deserialization_unknown_key_success(self): # pylint: disable=no-self-use from acme.messages import Directory - self.assertRaises( - jose.DeserializationError, Directory.from_json, {'foo': 'bar'}) + Directory.from_json({'foo': 'bar'}) class RegistrationTest(unittest.TestCase): From 7f70c09c5374ab225a341c73984ed12b3351abd7 Mon Sep 17 00:00:00 2001 From: chrismarget Date: Wed, 11 May 2016 15:19:39 -0400 Subject: [PATCH 084/396] Randomize serial numbers of DVSNI challenge certs. Should now work on python 2.7 and 3.3+ --- acme/acme/crypto_util.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/acme/acme/crypto_util.py b/acme/acme/crypto_util.py index 6cb5ad6fc..6465533a1 100644 --- a/acme/acme/crypto_util.py +++ b/acme/acme/crypto_util.py @@ -203,7 +203,12 @@ def gen_ss_cert(key, domains, not_before=None, """ assert domains, "Must provide one or more hostnames for the cert." cert = OpenSSL.crypto.X509() - cert.set_serial_number(int.from_bytes(OpenSSL.rand.bytes(16),'big')) + + try: + cert.set_serial_number(int(OpenSSL.rand.bytes(16).encode("hex"), 16)) + except AttributeError: + cert.set_serial_number(int.from_bytes(OpenSSL.rand.bytes(16),'big')) + cert.set_version(2) extensions = [ From 6fbd5fa81110e5e0c6aca139e4f827d2bab1da4e Mon Sep 17 00:00:00 2001 From: chrismarget Date: Wed, 11 May 2016 16:04:08 -0400 Subject: [PATCH 085/396] Added missing whitespace. --- acme/acme/crypto_util.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/acme/acme/crypto_util.py b/acme/acme/crypto_util.py index 6465533a1..66590bb26 100644 --- a/acme/acme/crypto_util.py +++ b/acme/acme/crypto_util.py @@ -203,12 +203,10 @@ def gen_ss_cert(key, domains, not_before=None, """ assert domains, "Must provide one or more hostnames for the cert." cert = OpenSSL.crypto.X509() - try: cert.set_serial_number(int(OpenSSL.rand.bytes(16).encode("hex"), 16)) except AttributeError: - cert.set_serial_number(int.from_bytes(OpenSSL.rand.bytes(16),'big')) - + cert.set_serial_number(int.from_bytes(OpenSSL.rand.bytes(16), 'big')) cert.set_version(2) extensions = [ From 4759bc9034a3e2dffbab561345a1716354e89b55 Mon Sep 17 00:00:00 2001 From: chrismarget Date: Wed, 11 May 2016 16:41:19 -0400 Subject: [PATCH 086/396] Trying to make pylint happy. --- acme/acme/crypto_util.py | 1 + 1 file changed, 1 insertion(+) diff --git a/acme/acme/crypto_util.py b/acme/acme/crypto_util.py index 66590bb26..40004c4d0 100644 --- a/acme/acme/crypto_util.py +++ b/acme/acme/crypto_util.py @@ -206,6 +206,7 @@ def gen_ss_cert(key, domains, not_before=None, try: cert.set_serial_number(int(OpenSSL.rand.bytes(16).encode("hex"), 16)) except AttributeError: + # pylint: disable=E1101 cert.set_serial_number(int.from_bytes(OpenSSL.rand.bytes(16), 'big')) cert.set_version(2) From f7b10bb83e01f21bf9aac06b6b7b78d65a1d279d Mon Sep 17 00:00:00 2001 From: chrismarget Date: Wed, 11 May 2016 17:06:29 -0400 Subject: [PATCH 087/396] Serial number randomization with improved portability. No exception handling required this time. --- acme/acme/crypto_util.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/acme/acme/crypto_util.py b/acme/acme/crypto_util.py index 40004c4d0..2b2133475 100644 --- a/acme/acme/crypto_util.py +++ b/acme/acme/crypto_util.py @@ -1,4 +1,5 @@ """Crypto utilities.""" +import binascii import contextlib import logging import re @@ -203,11 +204,7 @@ def gen_ss_cert(key, domains, not_before=None, """ assert domains, "Must provide one or more hostnames for the cert." cert = OpenSSL.crypto.X509() - try: - cert.set_serial_number(int(OpenSSL.rand.bytes(16).encode("hex"), 16)) - except AttributeError: - # pylint: disable=E1101 - cert.set_serial_number(int.from_bytes(OpenSSL.rand.bytes(16), 'big')) + cert.set_serial_number(int(binascii.hexlify(OpenSSL.rand.bytes(16)), 16)) cert.set_version(2) extensions = [ From 407ebad36e78c5efa93c56373b2018f07ce31dc5 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Wed, 11 May 2016 15:56:10 -0700 Subject: [PATCH 088/396] Support openssl and gpg signatures in parallel --- README.rst | 4 ++-- tools/release.sh | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 840cfff0a..2cec4adad 100644 --- a/README.rst +++ b/README.rst @@ -37,9 +37,9 @@ from your OS and puts others in a python virtual environment:: .. hint:: The certbot-auto download is protected by HTTPS, which is pretty good, but if you'd like to double check the integrity of the ``certbot-auto`` script, you can use these steps for verification before running it:: - user@server:~$ wget https://dl.eff.org/certbot-auto.sig + user@server:~$ wget https://dl.eff.org/certbot-auto.asc user@server:~$ gpg2 --recv-key A2CFB51FA275A7286234E7B24D17C995CD9775F2 - user@server:~$ gpg2 --trusted-key 4D17C995CD9775F2 --verify certbot-auto.sig certbot-auto + user@server:~$ gpg2 --trusted-key 4D17C995CD9775F2 --verify certbot-auto.asc certbot-auto And for full command line help, you can type:: diff --git a/tools/release.sh b/tools/release.sh index d41192af9..042aa5259 100755 --- a/tools/release.sh +++ b/tools/release.sh @@ -187,6 +187,9 @@ while ! openssl dgst -sha256 -verify $RELEASE_OPENSSL_PUBKEY -signature \ read -p "Please correctly sign letsencrypt-auto with offline-signrequest.sh" done +# This signature is not quite as strong, but easier for people to verify out of band +gpg -u "$RELEASE_GPG_KEY" --detach-sign --armor --sign letsencrypt-auto-source/letsencrypt-auto + # copy leauto to the root, overwriting the previous release version cp -p letsencrypt-auto-source/letsencrypt-auto letsencrypt-auto From ba7688ba99cf78f0ef1eff856965e06c4a3d43b3 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Wed, 11 May 2016 15:57:38 -0700 Subject: [PATCH 089/396] Avoid certbot-auto.asc.1 --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 2cec4adad..74cd838b0 100644 --- a/README.rst +++ b/README.rst @@ -37,7 +37,7 @@ from your OS and puts others in a python virtual environment:: .. hint:: The certbot-auto download is protected by HTTPS, which is pretty good, but if you'd like to double check the integrity of the ``certbot-auto`` script, you can use these steps for verification before running it:: - user@server:~$ wget https://dl.eff.org/certbot-auto.asc + user@server:~$ wget -N https://dl.eff.org/certbot-auto.asc user@server:~$ gpg2 --recv-key A2CFB51FA275A7286234E7B24D17C995CD9775F2 user@server:~$ gpg2 --trusted-key 4D17C995CD9775F2 --verify certbot-auto.asc certbot-auto From 5214c56f06c5202dfe2c77c15d2534daba17fd4c Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Wed, 11 May 2016 16:09:30 -0700 Subject: [PATCH 090/396] Use certbot-auto.asc --- tools/release.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tools/release.sh b/tools/release.sh index 2e26e3dab..8c2d04cd4 100755 --- a/tools/release.sh +++ b/tools/release.sh @@ -189,6 +189,9 @@ done # This signature is not quite as strong, but easier for people to verify out of band gpg -u "$RELEASE_GPG_KEY" --detach-sign --armor --sign letsencrypt-auto-source/letsencrypt-auto +# We can't rename the openssl letsencrypt-auto.sig for compatibility reasons, +# but we can use the right name for cerbot-auto.asc from day one +mv letsencrypt-auto-source/letsencrypt-auto.asc letsencrypt-auto-source/certbot-auto.asc # copy leauto to the root, overwriting the previous release version cp -p letsencrypt-auto-source/letsencrypt-auto certbot-auto From 8e742fa3c67a5f50dadd674904ef733ca3044d61 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 11 May 2016 18:04:15 -0700 Subject: [PATCH 091/396] Release 0.6.0 --- acme/setup.py | 2 +- certbot-apache/setup.py | 2 +- certbot-auto | 1088 +++++++++++++++++ certbot-compatibility-test/setup.py | 2 +- certbot-nginx/setup.py | 2 +- certbot/__init__.py | 2 +- letsencrypt-apache/setup.py | 2 +- letsencrypt-auto | 269 ++-- letsencrypt-auto-source/certbot-auto.asc | 11 + letsencrypt-auto-source/letsencrypt-auto | 26 +- letsencrypt-auto-source/letsencrypt-auto.sig | Bin 256 -> 256 bytes .../pieces/letsencrypt-auto-requirements.txt | 24 +- letsencrypt-nginx/setup.py | 2 +- letsencrypt/setup.py | 2 +- letshelp-certbot/setup.py | 2 +- letshelp-letsencrypt/setup.py | 2 +- 16 files changed, 1313 insertions(+), 125 deletions(-) create mode 100755 certbot-auto create mode 100644 letsencrypt-auto-source/certbot-auto.asc diff --git a/acme/setup.py b/acme/setup.py index cbd3bfb87..20c7c7226 100644 --- a/acme/setup.py +++ b/acme/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.6.0.dev0' +version = '0.6.0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot-apache/setup.py b/certbot-apache/setup.py index 7358c7041..538a68139 100644 --- a/certbot-apache/setup.py +++ b/certbot-apache/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.6.0.dev0' +version = '0.6.0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot-auto b/certbot-auto new file mode 100755 index 000000000..8c6e6c486 --- /dev/null +++ b/certbot-auto @@ -0,0 +1,1088 @@ +#!/bin/sh +# +# Download and run the latest release version of the Certbot client. +# +# NOTE: THIS SCRIPT IS AUTO-GENERATED AND SELF-UPDATING +# +# IF YOU WANT TO EDIT IT LOCALLY, *ALWAYS* RUN YOUR COPY WITH THE +# "--no-self-upgrade" FLAG +# +# IF YOU WANT TO SEND PULL REQUESTS, THE REAL SOURCE FOR THIS FILE IS +# letsencrypt-auto-source/letsencrypt-auto.template AND +# letsencrypt-auto-source/pieces/bootstrappers/* + +set -e # Work even if somebody does "sh thisscript.sh". + +# Note: you can set XDG_DATA_HOME or VENV_PATH before running this script, +# if you want to change where the virtual environment will be installed +XDG_DATA_HOME=${XDG_DATA_HOME:-~/.local/share} +VENV_NAME="letsencrypt" +VENV_PATH=${VENV_PATH:-"$XDG_DATA_HOME/$VENV_NAME"} +VENV_BIN="$VENV_PATH/bin" +LE_AUTO_VERSION="0.6.0" +BASENAME=$(basename $0) +USAGE="Usage: $BASENAME [OPTIONS] +A self-updating wrapper script for the Certbot ACME client. When run, updates +to both this script and certbot will be downloaded and installed. After +ensuring you have the latest versions installed, certbot will be invoked with +all arguments you have provided. + +Help for certbot itself cannot be provided until it is installed. + + --debug attempt experimental installation + -h, --help print this help + -n, --non-interactive, --noninteractive run without asking for user input + --no-self-upgrade do not download updates + --os-packages-only install OS dependencies and exit + -v, --verbose provide more output + +All arguments are accepted and forwarded to the Certbot client when run." + +while getopts ":hnv" arg; do + case $arg in + h) + HELP=1;; + n) + ASSUME_YES=1;; + v) + VERBOSE=1;; + esac +done + +for arg in "$@" ; do + case "$arg" in + --debug) + DEBUG=1;; + --os-packages-only) + OS_PACKAGES_ONLY=1;; + --no-self-upgrade) + # Do not upgrade this script (also prevents client upgrades, because each + # copy of the script pins a hash of the python client) + NO_SELF_UPGRADE=1;; + --help) + HELP=1;; + --noninteractive|--non-interactive) + ASSUME_YES=1;; + --verbose) + VERBOSE=1;; + esac +done + +# certbot-auto needs root access to bootstrap OS dependencies, and +# certbot itself needs root access for almost all modes of operation +# The "normal" case is that sudo is used for the steps that need root, but +# this script *can* be run as root (not recommended), or fall back to using +# `su` +SUDO_ENV="" +export CERTBOT_AUTO="$0" +if test "`id -u`" -ne "0" ; then + if command -v sudo 1>/dev/null 2>&1; then + SUDO=sudo + SUDO_ENV="CERTBOT_AUTO=$0" + else + echo \"sudo\" is not available, will use \"su\" for installation steps... + # Because the parameters in `su -c` has to be a string, + # we need properly escape it + su_sudo() { + args="" + # This `while` loop iterates over all parameters given to this function. + # For each parameter, all `'` will be replace by `'"'"'`, and the escaped string + # will be wrapped in a pair of `'`, then appended to `$args` string + # For example, `echo "It's only 1\$\!"` will be escaped to: + # 'echo' 'It'"'"'s only 1$!' + # │ │└┼┘│ + # │ │ │ └── `'s only 1$!'` the literal string + # │ │ └── `\"'\"` is a single quote (as a string) + # │ └── `'It'`, to be concatenated with the strings following it + # └── `echo` wrapped in a pair of `'`, it's totally fine for the shell command itself + while [ $# -ne 0 ]; do + args="$args'$(printf "%s" "$1" | sed -e "s/'/'\"'\"'/g")' " + shift + done + su root -c "$args" + } + SUDO=su_sudo + fi +else + SUDO= +fi + +if [ $BASENAME = "letsencrypt-auto" ]; then + # letsencrypt-auto does not respect --help or --yes for backwards compatibility + ASSUME_YES=1 + HELP=0 +fi + +ExperimentalBootstrap() { + # Arguments: Platform name, bootstrap function name + if [ "$DEBUG" = 1 ]; then + if [ "$2" != "" ]; then + echo "Bootstrapping dependencies via $1..." + $2 + fi + else + echo "WARNING: $1 support is very experimental at present..." + echo "if you would like to work on improving it, please ensure you have backups" + echo "and then run this script again with the --debug flag!" + exit 1 + fi +} + +DeterminePythonVersion() { + for LE_PYTHON in "$LE_PYTHON" python2.7 python27 python2 python; do + # Break (while keeping the LE_PYTHON value) if found. + command -v "$LE_PYTHON" > /dev/null && break + done + if [ "$?" != "0" ]; then + echo "Cannot find any Pythons; please install one!" + exit 1 + fi + export LE_PYTHON + + PYVER=`"$LE_PYTHON" -V 2>&1 | cut -d" " -f 2 | cut -d. -f1,2 | sed 's/\.//'` + if [ "$PYVER" -lt 26 ]; then + echo "You have an ancient version of Python entombed in your operating system..." + echo "This isn't going to work; you'll need at least version 2.6." + exit 1 + fi +} + +BootstrapDebCommon() { + # Current version tested with: + # + # - Ubuntu + # - 14.04 (x64) + # - 15.04 (x64) + # - Debian + # - 7.9 "wheezy" (x64) + # - sid (2015-10-21) (x64) + + # Past versions tested with: + # + # - Debian 8.0 "jessie" (x64) + # - Raspbian 7.8 (armhf) + + # Believed not to work: + # + # - Debian 6.0.10 "squeeze" (x64) + + $SUDO apt-get update || echo apt-get update hit problems but continuing anyway... + + # virtualenv binary can be found in different packages depending on + # distro version (#346) + + virtualenv= + if apt-cache show virtualenv > /dev/null 2>&1; then + virtualenv="virtualenv" + fi + + if apt-cache show python-virtualenv > /dev/null 2>&1; then + virtualenv="$virtualenv python-virtualenv" + fi + + augeas_pkg="libaugeas0 augeas-lenses" + AUGVERSION=`apt-cache show --no-all-versions libaugeas0 | grep ^Version: | cut -d" " -f2` + + if [ "$ASSUME_YES" = 1 ]; then + YES_FLAG="-y" + fi + + AddBackportRepo() { + # ARGS: + BACKPORT_NAME="$1" + BACKPORT_SOURCELINE="$2" + echo "To use the Apache Certbot plugin, augeas needs to be installed from $BACKPORT_NAME." + if ! grep -v -e ' *#' /etc/apt/sources.list | grep -q "$BACKPORT_NAME" ; then + # This can theoretically error if sources.list.d is empty, but in that case we don't care. + if ! grep -v -e ' *#' /etc/apt/sources.list.d/* 2>/dev/null | grep -q "$BACKPORT_NAME"; then + if [ "$ASSUME_YES" = 1 ]; then + /bin/echo -n "Installing augeas from $BACKPORT_NAME in 3 seconds..." + sleep 1s + /bin/echo -ne "\e[0K\rInstalling augeas from $BACKPORT_NAME in 2 seconds..." + sleep 1s + /bin/echo -e "\e[0K\rInstalling augeas from $BACKPORT_NAME in 1 second ..." + sleep 1s + add_backports=1 + else + read -p "Would you like to enable the $BACKPORT_NAME repository [Y/n]? " response + case $response in + [yY][eE][sS]|[yY]|"") + add_backports=1;; + *) + add_backports=0;; + esac + fi + if [ "$add_backports" = 1 ]; then + $SUDO sh -c "echo $BACKPORT_SOURCELINE >> /etc/apt/sources.list.d/$BACKPORT_NAME.list" + $SUDO apt-get update + fi + fi + fi + if [ "$add_backports" != 0 ]; then + $SUDO apt-get install $YES_FLAG --no-install-recommends -t "$BACKPORT_NAME" $augeas_pkg + augeas_pkg= + fi + } + + + if dpkg --compare-versions 1.0 gt "$AUGVERSION" ; then + if lsb_release -a | grep -q wheezy ; then + AddBackportRepo wheezy-backports "deb http://http.debian.net/debian wheezy-backports main" + elif lsb_release -a | grep -q precise ; then + # XXX add ARM case + AddBackportRepo precise-backports "deb http://archive.ubuntu.com/ubuntu precise-backports main restricted universe multiverse" + else + echo "No libaugeas0 version is available that's new enough to run the" + echo "Certbot apache plugin..." + fi + # XXX add a case for ubuntu PPAs + fi + + $SUDO apt-get install $YES_FLAG --no-install-recommends \ + python \ + python-dev \ + $virtualenv \ + gcc \ + dialog \ + $augeas_pkg \ + libssl-dev \ + libffi-dev \ + ca-certificates \ + + + + if ! command -v virtualenv > /dev/null ; then + echo Failed to install a working \"virtualenv\" command, exiting + exit 1 + fi +} + +BootstrapRpmCommon() { + # Tested with: + # - Fedora 20, 21, 22, 23 (x64) + # - Centos 7 (x64: on DigitalOcean droplet) + # - CentOS 7 Minimal install in a Hyper-V VM + # - CentOS 6 (EPEL must be installed manually) + + if type dnf 2>/dev/null + then + tool=dnf + elif type yum 2>/dev/null + then + tool=yum + + else + echo "Neither yum nor dnf found. Aborting bootstrap!" + exit 1 + fi + + pkgs=" + gcc + dialog + augeas-libs + openssl + openssl-devel + libffi-devel + redhat-rpm-config + ca-certificates + " + + # Some distros and older versions of current distros use a "python27" + # instead of "python" naming convention. Try both conventions. + if $SUDO $tool list python >/dev/null 2>&1; then + pkgs="$pkgs + python + python-devel + python-virtualenv + python-tools + python-pip + " + else + pkgs="$pkgs + python27 + python27-devel + python27-virtualenv + python27-tools + python27-pip + " + fi + + if $SUDO $tool list installed "httpd" >/dev/null 2>&1; then + pkgs="$pkgs + mod_ssl + " + fi + + if [ "$ASSUME_YES" = 1 ]; then + yes_flag="-y" + fi + + if ! $SUDO $tool install $yes_flag $pkgs; then + echo "Could not install OS dependencies. Aborting bootstrap!" + exit 1 + fi +} + +BootstrapSuseCommon() { + # SLE12 don't have python-virtualenv + + if [ "$ASSUME_YES" = 1 ]; then + zypper_flags="-nq" + install_flags="-l" + fi + + $SUDO zypper $zypper_flags in $install_flags \ + python \ + python-devel \ + python-virtualenv \ + gcc \ + dialog \ + augeas-lenses \ + libopenssl-devel \ + libffi-devel \ + ca-certificates +} + +BootstrapArchCommon() { + # Tested with: + # - ArchLinux (x86_64) + # + # "python-virtualenv" is Python3, but "python2-virtualenv" provides + # only "virtualenv2" binary, not "virtualenv" necessary in + # ./tools/_venv_common.sh + + deps=" + python2 + python-virtualenv + gcc + dialog + augeas + openssl + libffi + ca-certificates + pkg-config + " + + # pacman -T exits with 127 if there are missing dependencies + missing=$($SUDO pacman -T $deps) || true + + if [ "$ASSUME_YES" = 1 ]; then + noconfirm="--noconfirm" + fi + + if [ "$missing" ]; then + $SUDO pacman -S --needed $missing $noconfirm + fi +} + +BootstrapGentooCommon() { + PACKAGES=" + dev-lang/python:2.7 + dev-python/virtualenv + dev-util/dialog + app-admin/augeas + dev-libs/openssl + dev-libs/libffi + app-misc/ca-certificates + virtual/pkgconfig" + + case "$PACKAGE_MANAGER" in + (paludis) + $SUDO cave resolve --preserve-world --keep-targets if-possible $PACKAGES -x + ;; + (pkgcore) + $SUDO pmerge --noreplace --oneshot $PACKAGES + ;; + (portage|*) + $SUDO emerge --noreplace --oneshot $PACKAGES + ;; + esac +} + +BootstrapFreeBsd() { + $SUDO pkg install -Ay \ + python \ + py27-virtualenv \ + augeas \ + libffi +} + +BootstrapMac() { + if hash brew 2>/dev/null; then + echo "Using Homebrew to install dependencies..." + pkgman=brew + pkgcmd="brew install" + elif hash port 2>/dev/null; then + echo "Using MacPorts to install dependencies..." + pkgman=port + pkgcmd="$SUDO port install" + else + echo "No Homebrew/MacPorts; installing Homebrew..." + ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)" + pkgman=brew + pkgcmd="brew install" + fi + + $pkgcmd augeas + $pkgcmd dialog + if [ "$(which python)" = "/System/Library/Frameworks/Python.framework/Versions/2.7/bin/python" ]; then + # We want to avoid using the system Python because it requires root to use pip. + # python.org, MacPorts or HomeBrew Python installations should all be OK. + echo "Installing python..." + $pkgcmd python + fi + + # Workaround for _dlopen not finding augeas on OS X + if [ "$pkgman" = "port" ] && ! [ -e "/usr/local/lib/libaugeas.dylib" ] && [ -e "/opt/local/lib/libaugeas.dylib" ]; then + echo "Applying augeas workaround" + $SUDO ln -s /opt/local/lib/libaugeas.dylib /usr/local/lib + fi + + if ! hash pip 2>/dev/null; then + echo "pip not installed" + echo "Installing pip..." + curl --silent --show-error --retry 5 https://bootstrap.pypa.io/get-pip.py | python + fi + + if ! hash virtualenv 2>/dev/null; then + echo "virtualenv not installed." + echo "Installing with pip..." + pip install virtualenv + fi +} + + +# Install required OS packages: +Bootstrap() { + if [ -f /etc/debian_version ]; then + echo "Bootstrapping dependencies for Debian-based OSes..." + BootstrapDebCommon + elif [ -f /etc/redhat-release ]; then + echo "Bootstrapping dependencies for RedHat-based OSes..." + BootstrapRpmCommon + elif [ -f /etc/os-release ] && `grep -q openSUSE /etc/os-release` ; then + echo "Bootstrapping dependencies for openSUSE-based OSes..." + BootstrapSuseCommon + elif [ -f /etc/arch-release ]; then + if [ "$DEBUG" = 1 ]; then + echo "Bootstrapping dependencies for Archlinux..." + BootstrapArchCommon + else + echo "Please use pacman to install letsencrypt packages:" + echo "# pacman -S letsencrypt letsencrypt-apache" + echo + echo "If you would like to use the virtualenv way, please run the script again with the" + echo "--debug flag." + exit 1 + fi + elif [ -f /etc/manjaro-release ]; then + ExperimentalBootstrap "Manjaro Linux" BootstrapArchCommon + elif [ -f /etc/gentoo-release ]; then + ExperimentalBootstrap "Gentoo" BootstrapGentooCommon + elif uname | grep -iq FreeBSD ; then + ExperimentalBootstrap "FreeBSD" BootstrapFreeBsd + elif uname | grep -iq Darwin ; then + ExperimentalBootstrap "Mac OS X" BootstrapMac + elif grep -iq "Amazon Linux" /etc/issue ; then + ExperimentalBootstrap "Amazon Linux" BootstrapRpmCommon + else + echo "Sorry, I don't know how to bootstrap Certbot on your operating system!" + echo + echo "You will need to bootstrap, configure virtualenv, and run pip install manually." + echo "Please see https://letsencrypt.readthedocs.org/en/latest/contributing.html#prerequisites" + echo "for more info." + fi +} + +TempDir() { + mktemp -d 2>/dev/null || mktemp -d -t 'le' # Linux || OS X +} + + + +if [ "$1" = "--le-auto-phase2" ]; then + # Phase 2: Create venv, install LE, and run. + + shift 1 # the --le-auto-phase2 arg + if [ -f "$VENV_BIN/letsencrypt" ]; then + # --version output ran through grep due to python-cryptography DeprecationWarnings + # grep for both certbot and letsencrypt until certbot and shim packages have been released + INSTALLED_VERSION=$("$VENV_BIN/letsencrypt" --version 2>&1 | grep "^certbot\|^letsencrypt" | cut -d " " -f 2) + else + INSTALLED_VERSION="none" + fi + if [ "$LE_AUTO_VERSION" != "$INSTALLED_VERSION" ]; then + echo "Creating virtual environment..." + DeterminePythonVersion + rm -rf "$VENV_PATH" + if [ "$VERBOSE" = 1 ]; then + virtualenv --no-site-packages --python "$LE_PYTHON" "$VENV_PATH" + else + virtualenv --no-site-packages --python "$LE_PYTHON" "$VENV_PATH" > /dev/null + fi + + echo "Installing Python packages..." + TEMP_DIR=$(TempDir) + # There is no $ interpolation due to quotes on starting heredoc delimiter. + # ------------------------------------------------------------------------- + cat << "UNLIKELY_EOF" > "$TEMP_DIR/letsencrypt-auto-requirements.txt" +# This is the flattened list of packages certbot-auto installs. To generate +# this, do `pip install --no-cache-dir -e acme -e . -e certbot-apache`, and +# then use `hashin` or a more secure method to gather the hashes. + +argparse==1.4.0 \ + --hash=sha256:c31647edb69fd3d465a847ea3157d37bed1f95f19760b11a47aa91c04b666314 \ + --hash=sha256:62b089a55be1d8949cd2bc7e0df0bddb9e028faefc8c32038cc84862aefdd6e4 + +# This comes before cffi because cffi will otherwise install an unchecked +# version via setup_requires. +pycparser==2.14 \ + --hash=sha256:7959b4a74abdc27b312fed1c21e6caf9309ce0b29ea86b591fd2e99ecdf27f73 + +cffi==1.4.2 \ + --hash=sha256:53c1c9ddb30431513eb7f3cdef0a3e06b0f1252188aaa7744af0f5a4cd45dbaf \ + --hash=sha256:a568f49dfca12a8d9f370187257efc58a38109e1eee714d928561d7a018a64f8 \ + --hash=sha256:809c6ca8cfbcaeebfbd432b4576001b40d38ff2463773cb57577d75e1a020bc3 \ + --hash=sha256:86cdca2cd9cba41422230390df17dfeaa9f344a911e3975c8be9da57b35548e9 \ + --hash=sha256:24b13db84aec385ca23c7b8ded83ef8bb4177bc181d14758f9f975be5d020d86 \ + --hash=sha256:969aeffd7c0e097f6be1efd682c156ae226591a0793a94b6c2d5e4293f4c8d4e \ + --hash=sha256:000f358d4b0fa249feaab9c1ce7d5b2fe7e02e7bdf6806c26418505fc685e268 \ + --hash=sha256:a9d86f460bbd8358a2d513ad779e3f3fc878e3b93a00b5002faebf616ffe6b9c \ + --hash=sha256:3127b3ab33eb23ccac071f9a0802748e5cf7c5cbcd02482bb063e35b41dbb0b0 \ + --hash=sha256:e2b2d42236469a40224d39e7b6c60575f388b2f423f354c7ee90a5b7f58c8065 \ + --hash=sha256:8c2dccafee89b1b424b0bec6ad2dd9622c949d2024e929f5da1ed801eac75f1d \ + --hash=sha256:a4de7a4d11aed488bab4fb14f4988587a829bece5a20433f780d6e33b08083cb \ + --hash=sha256:5ca8fe30425265a49274e4b0213a1bc98f4b13449ae5e96f984771e5d83e58c1 \ + --hash=sha256:a4fd38802f59e714eba81a024f62db710b27dbe27a7ea12e911537327aa84d30 \ + --hash=sha256:86cd6912bbc83e9405d4a73cd7f4b4ee8353652d2dbc7c820106ed5b4d1bab3a \ + --hash=sha256:8f1d177d364ea35900415ae24ca3e471be3d5334ed0419294068c49f45913998 +ConfigArgParse==0.10.0 \ + --hash=sha256:3b50a83dd58149dfcee98cb6565265d10b53e9c0a2bca7eeef7fb5f5524890a7 +configobj==5.0.6 \ + --hash=sha256:a2f5650770e1c87fb335af19a9b7eb73fc05ccf22144eb68db7d00cd2bcb0902 +cryptography==1.2.3 \ + --hash=sha256:031938f73a5c5eb3e809e18ff7caeb6865351871417be6050cb8c86a9a202b9a \ + --hash=sha256:a179a38d50f8d68b491d7a313db78f8cabe290842cecddddc7b34d408e59db0a \ + --hash=sha256:906c88b2aadcf99cfabb24098263d1bf65ab0c8688acde10dae1f09d865920f1 \ + --hash=sha256:6e706c5c6088770b1d1b634e959e21963e315b0255f5f4777125ad3d54082977 \ + --hash=sha256:f5ebf8e31c48f8707921dca0e994de77813a9c9b9bf03c119c5ddf97bdcffe73 \ + --hash=sha256:c7b89e42288cc7fbee3812e99ef5c744f22452e11d6822f6807afc6d6b3be83e \ + --hash=sha256:8408d29865947109d8b68f1837a7cde1aa4dc86e0f79ca3ba58c0c44e443d6a5 \ + --hash=sha256:c7e76cf3c3d925dd31fa238cfb806cffba718c0f08707d77a538768477969956 \ + --hash=sha256:7d8de35380f31702758b7753bb5c40723832c73006dedb2f9099bf61a37f7287 \ + --hash=sha256:5edbee71fae5469ee83fe0a37866b9398c8ce3a46325c24fcedfbf097bb48a19 \ + --hash=sha256:594edafe4801c13bdc1cc305e7704a90c19617e95936f6ab457ee4ffe000ba50 \ + --hash=sha256:b7fdb16a0a7f481be42da744bfe1ea2163025de21f90f2c688a316f3c354da9c \ + --hash=sha256:207b8bf0fe0907336df38b733b487521cf9e138189aba9234ad54fe545dd0db8 \ + --hash=sha256:509a2f05386270cf783993c90d49ffefb3dd62aee45bf1ea8ce3d2cde7271c21 \ + --hash=sha256:ac69b65dd1af0179ede40c9f15788c88f73e628ea6c0519de3838e279bb388c6 \ + --hash=sha256:8df6fad6c6ae12fd7004ea29357f0a2b4d3774eaeca7656530d08d2d90cd41aa \ + --hash=sha256:0b8b96dd81cc1533a04f30382c0fe21c1972e189f794d0c4261a18cec08fd9b5 \ + --hash=sha256:cae8fca1883f23c50ea78d89de6fe4fefdb4cea83177760f47177559414ded93 \ + --hash=sha256:1a471ca576a9cdce1b1cd9f3a22b1d09ee44d46862037557de17919c0db44425 \ + --hash=sha256:8ec4e8e3d453b3a1b63b5f57737a434dcf1ee4a2f26f6ff7c5a37c3f679104d2 \ + --hash=sha256:8eb11c77dd8e73f48df6b2f7a7e16173fe0fe8fdfe266232832e88477e08454e +enum34==1.1.2 \ + --hash=sha256:2475d7fcddf5951e92ff546972758802de5260bf409319a9f1934e6bbc8b1dc7 \ + --hash=sha256:35907defb0f992b75ab7788f65fedc1cf20ffa22688e0e6f6f12afc06b3ea501 +funcsigs==0.4 \ + --hash=sha256:ff5ad9e2f8d9e5d1e8bbfbcf47722ab527cf0d51caeeed9da6d0f40799383fde \ + --hash=sha256:d83ce6df0b0ea6618700fe1db353526391a8a3ada1b7aba52fed7a61da772033 +idna==2.0 \ + --hash=sha256:9b2fc50bd3c4ba306b9651b69411ef22026d4d8335b93afc2214cef1246ce707 \ + --hash=sha256:16199aad938b290f5be1057c0e1efc6546229391c23cea61ca940c115f7d3d3b +ipaddress==1.0.16 \ + --hash=sha256:935712800ce4760701d89ad677666cd52691fd2f6f0b340c8b4239a3c17988a5 \ + --hash=sha256:5a3182b322a706525c46282ca6f064d27a02cffbd449f9f47416f1dc96aa71b0 +linecache2==1.0.0 \ + --hash=sha256:e78be9c0a0dfcbac712fe04fbf92b96cddae80b1b842f24248214c8496f006ef \ + --hash=sha256:4b26ff4e7110db76eeb6f5a7b64a82623839d595c2038eeda662f2a2db78e97c +ndg-httpsclient==0.4.0 \ + --hash=sha256:e8c155fdebd9c4bcb0810b4ed01ae1987554b1ee034dd7532d7b8fdae38a6274 +ordereddict==1.1 \ + --hash=sha256:1c35b4ac206cef2d24816c89f89cf289dd3d38cf7c449bb3fab7bf6d43f01b1f +parsedatetime==2.1 \ + --hash=sha256:ce9d422165cf6e963905cd5f74f274ebf7cc98c941916169178ef93f0e557838 \ + --hash=sha256:17c578775520c99131634e09cfca5a05ea9e1bd2a05cd06967ebece10df7af2d +pbr==1.8.1 \ + --hash=sha256:46c8db75ae75a056bd1cc07fa21734fe2e603d11a07833ecc1eeb74c35c72e0c \ + --hash=sha256:e2127626a91e6c885db89668976db31020f0af2da728924b56480fc7ccf09649 +psutil==3.3.0 \ + --hash=sha256:584f0b29fcc5d523b433cb8918b2fc74d67e30ee0b44a95baf031528f424619f \ + --hash=sha256:28ca0b6e9d99aa8dc286e8747a4471362b69812a25291de29b6a8d70a1545a0d \ + --hash=sha256:167ad5fff52a672c4ddc1c1a0b25146d6813ebb08a9aab0a3ac45f8a5b669c3b \ + --hash=sha256:e6dea6173a988727bb223d3497349ad5cdef5c0b282eff2d83e5f9065c53f85f \ + --hash=sha256:2af5e0a4aad66049955d0734aa4e3dc8caa17a9eaf8b4c1a27a5f1ee6e40f6fc \ + --hash=sha256:d9884dc0dc2e55e2448e495778dc9899c1c8bf37aeb2f434c1bea74af93c2683 \ + --hash=sha256:e27c2fe6dfcc8738be3d2c5a022f785eb72971057e1a9e1e34fba73bce8a71a6 \ + --hash=sha256:65afd6fecc8f3aed09ee4be63583bc8eb472f06ceaa4fe24c4d1d5a1a3c0e13f \ + --hash=sha256:ba1c558fbfcdf94515c2394b1155c1dc56e2bc2a9c17d30349827c9ed8a67e46 \ + --hash=sha256:ba95ea0022dcb64d36f0c1335c0605fae35bdf3e0fea8d92f5d0f6456a35e55b \ + --hash=sha256:421b6591d16b509aaa8d8c15821d66bb94cb4a8dc4385cad5c51b85d4a096d85 \ + --hash=sha256:326b305cbdb6f94dafbfe2c26b11da88b0ab07b8a07f8188ab9d75ff0c6e841a \ + --hash=sha256:9aede5b2b6fe46b3748ea8e5214443890d1634027bef3d33b7dad16556830278 \ + --hash=sha256:73bed1db894d1aa9c3c7e611d302cdeab7ae8a0dc0eeaf76727878db1ac5cd87 \ + --hash=sha256:935b5dd6d558af512f42501a7c08f41d7aff139af1bb3959daa3abb859234d6c \ + --hash=sha256:4ca0111cf157dcc0f2f69a323c5b5478718d68d45fc9435d84be0ec0f186215b \ + --hash=sha256:b6f13c95398a3fcf0226c4dcfa448560ba5865259cd96ec2810658651e932189 \ + --hash=sha256:ee6be30d1635bbdea4c4325d507dc8a0dbbde7e1c198bd62ddb9f43198b9e214 \ + --hash=sha256:dfa786858c268d7fbbe1b6175e001ec02738d7cfae0a7ce77bf9b651af676729 \ + --hash=sha256:aa77f9de72af9c16cc288cd4a24cf58824388f57d7a81e400c4616457629870e \ + --hash=sha256:f500093357d04da8140d87932cac2e54ef592a54ca8a743abb2850f60c2c22eb +pyasn1==0.1.9 \ + --hash=sha256:61f9d99e3cef65feb1bfe3a2eef7a93eb93819d345bf54bcd42f4e63d5204dae \ + --hash=sha256:1802a6dd32045e472a419db1441aecab469d33e0d2749e192abdec52101724af \ + --hash=sha256:35025cd9422c96504912f04e2f15fe79390a8597b430c2ca5d0534cf9309ffa0 \ + --hash=sha256:2f96ed5a0c329ca16230b326ca12b7461ec8f65e0be3e4f997516f36bf82a345 \ + --hash=sha256:28fee44217991cfad9e6a0b9f7e3f26041e21ebc96629e94e585ccd05d49fa65 \ + --hash=sha256:326e7a854a17fab07691204747695f8f692d674588a355c441fb14f660bf4e68 \ + --hash=sha256:cda5a90485709ca6795c86056c3e5fe7266028b05e53f1d527fdf93a6365a6b8 \ + --hash=sha256:0cb2a14742b543fdd68f931a14ce3829186ed2b1b2267a06787388c96b2dd9be \ + --hash=sha256:5191ff6b9126d2c039dd87f8ff025bed274baf07fa78afa46f556b1ad7265d6e \ + --hash=sha256:8323e03637b2d072cc7041300bac6ec448c3c28950ab40376036788e9a1af629 \ + --hash=sha256:853cacd96d1f701ddd67aa03ecc05f51890135b7262e922710112f12a2ed2a7f +pyOpenSSL==0.15.1 \ + --hash=sha256:88e45e6bb25dfed272a1ef2e728461d44b634c2cd689e989b6e56a349c5a3ae5 \ + --hash=sha256:f0a26070d6db0881de8bcc7846934b7c3c930d8f9c79d45883ee48984bc0d672 +pyRFC3339==1.0 \ + --hash=sha256:eea31835c56e2096af4363a5745a784878a61d043e247d3a6d6a0a32a9741f56 \ + --hash=sha256:8dfbc6c458b8daba1c0f3620a8c78008b323a268b27b7359e92a4ae41325f535 +python-augeas==0.5.0 \ + --hash=sha256:67d59d66cdba8d624e0389b87b2a83a176f21f16a87553b50f5703b23f29bac2 +python2-pythondialog==3.3.0 \ + --hash=sha256:04e93f24995c43dd90f338d5d865ca72ce3fb5a5358d4daa4965571db35fc3ec \ + --hash=sha256:3e6f593fead98f8a526bc3e306933533236e33729f552f52896ea504f55313fa +pytz==2015.7 \ + --hash=sha256:3abe6a6d3fc2fbbe4c60144211f45da2edbe3182a6f6511af6bbba0598b1f992 \ + --hash=sha256:939ef9c1e1224d980405689a97ffcf7828c56d1517b31d73464356c1f2b7769e \ + --hash=sha256:ead4aefa7007249e05e51b01095719d5a8dd95760089f5730aac5698b1932918 \ + --hash=sha256:3cca0df08bd0ed98432390494ce3ded003f5e661aa460be7a734bffe35983605 \ + --hash=sha256:3ede470d3d17ba3c07638dfa0d10452bc1b6e5ad326127a65ba77e6aaeb11bec \ + --hash=sha256:68c47964f7186eec306b13629627722b9079cd4447ed9e5ecaecd4eac84ca734 \ + --hash=sha256:dd5d3991950aae40a6c81de1578942e73d629808cefc51d12cd157980e6cfc18 \ + --hash=sha256:a77c52062c07eb7c7b30545dbc73e32995b7e117eea750317b5cb5c7a4618f14 \ + --hash=sha256:81af9aec4bc960a9a0127c488f18772dae4634689233f06f65443e7b11ebeb51 \ + --hash=sha256:e079b1dadc5c06246cc1bb6fe1b23a50b1d1173f2edd5104efd40bb73a28f406 \ + --hash=sha256:fbd26746772c24cb93c8b97cbdad5cb9e46c86bbdb1b9d8a743ee00e2fb1fc5d \ + --hash=sha256:99266ef30a37e43932deec2b7ca73e83c8dbc3b9ff703ec73eca6b1dae6befea \ + --hash=sha256:8b6ce1c993909783bc96e0b4f34ea223bff7a4df2c90bdb9c4e0f1ac928689e3 +requests==2.9.1 \ + --hash=sha256:113fbba5531a9e34945b7d36b33a084e8ba5d0664b703c81a7c572d91919a5b8 \ + --hash=sha256:c577815dd00f1394203fc44eb979724b098f88264a9ef898ee45b8e5e9cf587f +six==1.10.0 \ + --hash=sha256:0ff78c403d9bccf5a425a6d31a12aa6b47f1c21ca4dc2573a7e2f32a97335eb1 \ + --hash=sha256:105f8d68616f8248e24bf0e9372ef04d3cc10104f1980f54d57b2ce73a5ad56a +traceback2==1.4.0 \ + --hash=sha256:8253cebec4b19094d67cc5ed5af99bf1dba1285292226e98a31929f87a5d6b23 \ + --hash=sha256:05acc67a09980c2ecfedd3423f7ae0104839eccb55fc645773e1caa0951c3030 +unittest2==1.1.0 \ + --hash=sha256:13f77d0875db6d9b435e1d4f41e74ad4cc2eb6e1d5c824996092b3430f088bb8 \ + --hash=sha256:22882a0e418c284e1f718a822b3b022944d53d2d908e1690b319a9d3eb2c0579 +zope.component==4.2.2 \ + --hash=sha256:282c112b55dd8e3c869a3571f86767c150ab1284a9ace2bdec226c592acaf81a +zope.event==4.1.0 \ + --hash=sha256:dc7a59a2fd91730d3793131a5d261b29e93ec4e2a97f1bc487ce8defee2fe786 +zope.interface==4.1.3 \ + --hash=sha256:f07b631f7a601cd8cbd3332d54f43142c7088a83299f859356f08d1d4d4259b3 \ + --hash=sha256:de5cca083b9439d8002fb76bbe6b4998c5a5a721fab25b84298967f002df4c94 \ + --hash=sha256:6788416f7ea7f5b8a97be94825377aa25e8bdc73463e07baaf9858b29e737077 \ + --hash=sha256:6f3230f7254518201e5a3708cbb2de98c848304f06e3ded8bfb39e5825cba2e1 \ + --hash=sha256:5fa575a5240f04200c3088427d0d4b7b737f6e9018818a51d8d0f927a6a2517a \ + --hash=sha256:522194ad6a545735edd75c8a83f48d65d1af064e432a7d320d64f56bafc12e99 \ + --hash=sha256:e8c7b2d40943f71c99148c97f66caa7f5134147f57423f8db5b4825099ce9a09 \ + --hash=sha256:279024f0208601c3caa907c53876e37ad88625f7eaf1cb3842dbe360b2287017 \ + --hash=sha256:2e221a9eec7ccc58889a278ea13dcfed5ef939d80b07819a9a8b3cb1c681484f \ + --hash=sha256:69118965410ec86d44dc6b9017ee3ddbd582e0c0abeef62b3a19dbf6c8ad132b \ + --hash=sha256:d04df8686ec864d0cade8cf199f7f83aecd416109a20834d568f8310ded12dea \ + --hash=sha256:e75a947e15ee97e7e71e02ea302feb2fc62d3a2bb4668bf9dfbed43a506ac7e7 \ + --hash=sha256:4e45d22fb883222a5ab9f282a116fec5ee2e8d1a568ccff6a2d75bbd0eb6bcfc \ + --hash=sha256:bce9339bb3c7a55e0803b63d21c5839e8e479bc85c4adf42ae415b72f94facb2 \ + --hash=sha256:928138365245a0e8869a5999fbcc2a45475a0a6ed52a494d60dbdc540335fedd \ + --hash=sha256:0d841ba1bb840eea0e6489dc5ecafa6125554971f53b5acb87764441e61bceba \ + --hash=sha256:b09c8c1d47b3531c400e0195697f1414a63221de6ef478598a4f1460f7d9a392 +mock==1.0.1 \ + --hash=sha256:b839dd2d9c117c701430c149956918a423a9863b48b09c90e30a6013e7d2f44f \ + --hash=sha256:8f83080daa249d036cbccfb8ae5cc6ff007b88d6d937521371afabe7b19badbc + +# THE LINES BELOW ARE EDITED BY THE RELEASE SCRIPT; ADD ALL DEPENDENCIES ABOVE. + +acme==0.6.0 \ + --hash=sha256:cbe4e7a340a19725a8740ed86e30abdbe18fc22c4c6022b7a8e56642d502bcc3 \ + --hash=sha256:ec4e6009dfbd629b58473eb06bbebfd9fb2a79fc8831c149e9205bc38a98ecc6 +certbot==0.6.0 \ + --hash=sha256:a893632d228864b0a751db9f3fdd93439ed34b988ea21b64fb0f0fa2ceded6a2 \ + --hash=sha256:80b0b7dc5afeec2816ef638a61e7c628d73cd72666eebf4984be426d1c2b492d +certbot-apache==0.6.0 \ + --hash=sha256:0ab077f0913b81ed5c1b141c3a7c4c0228ef3738d8d61a93db794d9a80718d43 \ + --hash=sha256:1cfbe751209079a803758f472200816fac559f2a36fdd582d25e3ba5601423a1 +letsencrypt==0.6.0 \ + --hash=sha256:93196c7dcd57272a753e525d145c5a9987c8968c22ec954bcf83dcc9d2499a76 \ + --hash=sha256:a16d6c395f1bf5fd61a28ef83dc78f42dbecbad9d00be6236f2ad8915645c154 +letsencrypt-apache==0.6.0 \ + --hash=sha256:02fadc52a0796e53978c508beec9c53e1fc047660240832b9bde5d53ab3a1379 \ + --hash=sha256:1c5522d94d7750bdb9bfa6201d2c263e914f662c9d0079e673167233cf4364f1 + +UNLIKELY_EOF + # ------------------------------------------------------------------------- + cat << "UNLIKELY_EOF" > "$TEMP_DIR/pipstrap.py" +#!/usr/bin/env python +"""A small script that can act as a trust root for installing pip 8 + +Embed this in your project, and your VCS checkout is all you have to trust. In +a post-peep era, this lets you claw your way to a hash-checking version of pip, +with which you can install the rest of your dependencies safely. All it assumes +is Python 2.6 or better and *some* version of pip already installed. If +anything goes wrong, it will exit with a non-zero status code. + +""" +# This is here so embedded copies are MIT-compliant: +# Copyright (c) 2016 Erik Rose +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to +# deal in the Software without restriction, including without limitation the +# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +# sell copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +from __future__ import print_function +from hashlib import sha256 +from os.path import join +from pipes import quote +from shutil import rmtree +try: + from subprocess import check_output +except ImportError: + from subprocess import CalledProcessError, PIPE, Popen + + def check_output(*popenargs, **kwargs): + if 'stdout' in kwargs: + raise ValueError('stdout argument not allowed, it will be ' + 'overridden.') + process = Popen(stdout=PIPE, *popenargs, **kwargs) + output, unused_err = process.communicate() + retcode = process.poll() + if retcode: + cmd = kwargs.get("args") + if cmd is None: + cmd = popenargs[0] + raise CalledProcessError(retcode, cmd) + return output +from sys import exit, version_info +from tempfile import mkdtemp +try: + from urllib2 import build_opener, HTTPHandler, HTTPSHandler +except ImportError: + from urllib.request import build_opener, HTTPHandler, HTTPSHandler +try: + from urlparse import urlparse +except ImportError: + from urllib.parse import urlparse # 3.4 + + +__version__ = 1, 1, 1 + + +# wheel has a conditional dependency on argparse: +maybe_argparse = ( + [('https://pypi.python.org/packages/source/a/argparse/' + 'argparse-1.4.0.tar.gz', + '62b089a55be1d8949cd2bc7e0df0bddb9e028faefc8c32038cc84862aefdd6e4')] + if version_info < (2, 7, 0) else []) + + +PACKAGES = maybe_argparse + [ + # Pip has no dependencies, as it vendors everything: + ('https://pypi.python.org/packages/source/p/pip/pip-8.0.3.tar.gz', + '30f98b66f3fe1069c529a491597d34a1c224a68640c82caf2ade5f88aa1405e8'), + # This version of setuptools has only optional dependencies: + ('https://pypi.python.org/packages/source/s/setuptools/' + 'setuptools-20.2.2.tar.gz', + '24fcfc15364a9fe09a220f37d2dcedc849795e3de3e4b393ee988e66a9cbd85a'), + ('https://pypi.python.org/packages/source/w/wheel/wheel-0.29.0.tar.gz', + '1ebb8ad7e26b448e9caa4773d2357849bf80ff9e313964bcaf79cbf0201a1648') +] + + +class HashError(Exception): + def __str__(self): + url, path, actual, expected = self.args + return ('{url} did not match the expected hash {expected}. Instead, ' + 'it was {actual}. The file (left at {path}) may have been ' + 'tampered with.'.format(**locals())) + + +def hashed_download(url, temp, digest): + """Download ``url`` to ``temp``, make sure it has the SHA-256 ``digest``, + and return its path.""" + # Based on pip 1.4.1's URLOpener but with cert verification removed. Python + # >=2.7.9 verifies HTTPS certs itself, and, in any case, the cert + # authenticity has only privacy (not arbitrary code execution) + # implications, since we're checking hashes. + def opener(): + opener = build_opener(HTTPSHandler()) + # Strip out HTTPHandler to prevent MITM spoof: + for handler in opener.handlers: + if isinstance(handler, HTTPHandler): + opener.handlers.remove(handler) + return opener + + def read_chunks(response, chunk_size): + while True: + chunk = response.read(chunk_size) + if not chunk: + break + yield chunk + + response = opener().open(url) + path = join(temp, urlparse(url).path.split('/')[-1]) + actual_hash = sha256() + with open(path, 'wb') as file: + for chunk in read_chunks(response, 4096): + file.write(chunk) + actual_hash.update(chunk) + + actual_digest = actual_hash.hexdigest() + if actual_digest != digest: + raise HashError(url, path, actual_digest, digest) + return path + + +def main(): + temp = mkdtemp(prefix='pipstrap-') + try: + downloads = [hashed_download(url, temp, digest) + for url, digest in PACKAGES] + check_output('pip install --no-index --no-deps -U ' + + ' '.join(quote(d) for d in downloads), + shell=True) + except HashError as exc: + print(exc) + except Exception: + rmtree(temp) + raise + else: + rmtree(temp) + return 0 + return 1 + + +if __name__ == '__main__': + exit(main()) + +UNLIKELY_EOF + # ------------------------------------------------------------------------- + # Set PATH so pipstrap upgrades the right (v)env: + PATH="$VENV_BIN:$PATH" "$VENV_BIN/python" "$TEMP_DIR/pipstrap.py" + set +e + PIP_OUT=`"$VENV_BIN/pip" install --no-cache-dir --require-hashes -r "$TEMP_DIR/letsencrypt-auto-requirements.txt" 2>&1` + PIP_STATUS=$? + set -e + rm -rf "$TEMP_DIR" + if [ "$PIP_STATUS" != 0 ]; then + # Report error. (Otherwise, be quiet.) + echo "Had a problem while installing Python packages:" + echo "$PIP_OUT" + rm -rf "$VENV_PATH" + exit 1 + fi + echo "Installation succeeded." + fi + echo "Requesting root privileges to run certbot..." + if [ -z "$SUDO_ENV" ] ; then + # SUDO is su wrapper / noop + echo " " $SUDO "$VENV_BIN/letsencrypt" "$@" + $SUDO "$VENV_BIN/letsencrypt" "$@" + else + # sudo + echo " " $SUDO "$SUDO_ENV" "$VENV_BIN/letsencrypt" "$@" + $SUDO "$SUDO_ENV" "$VENV_BIN/letsencrypt" "$@" + fi + +else + # Phase 1: Upgrade certbot-auto if neceesary, then self-invoke. + # + # Each phase checks the version of only the thing it is responsible for + # upgrading. Phase 1 checks the version of the latest release of + # certbot-auto (which is always the same as that of the certbot + # package). Phase 2 checks the version of the locally installed certbot. + + if [ ! -f "$VENV_BIN/letsencrypt" ]; then + if [ "$HELP" = 1 ]; then + echo "$USAGE" + exit 0 + fi + # If it looks like we've never bootstrapped before, bootstrap: + Bootstrap + fi + if [ "$OS_PACKAGES_ONLY" = 1 ]; then + echo "OS packages installed." + exit 0 + fi + + if [ "$NO_SELF_UPGRADE" != 1 ]; then + echo "Checking for new version..." + TEMP_DIR=$(TempDir) + # --------------------------------------------------------------------------- + cat << "UNLIKELY_EOF" > "$TEMP_DIR/fetch.py" +"""Do downloading and JSON parsing without additional dependencies. :: + + # Print latest released version of LE to stdout: + python fetch.py --latest-version + + # Download letsencrypt-auto script from git tag v1.2.3 into the folder I'm + # in, and make sure its signature verifies: + python fetch.py --le-auto-script v1.2.3 + +On failure, return non-zero. + +""" +from distutils.version import LooseVersion +from json import loads +from os import devnull, environ +from os.path import dirname, join +import re +from subprocess import check_call, CalledProcessError +from sys import argv, exit +from urllib2 import build_opener, HTTPHandler, HTTPSHandler, HTTPError + +PUBLIC_KEY = environ.get('LE_AUTO_PUBLIC_KEY', """-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA6MR8W/galdxnpGqBsYbq +OzQb2eyW15YFjDDEMI0ZOzt8f504obNs920lDnpPD2/KqgsfjOgw2K7xWDJIj/18 +xUvWPk3LDkrnokNiRkA3KOx3W6fHycKL+zID7zy+xZYBuh2fLyQtWV1VGQ45iNRp +9+Zo7rH86cdfgkdnWTlNSHyTLW9NbXvyv/E12bppPcEvgCTAQXgnDVJ0/sqmeiij +n9tTFh03aM+R2V/21h8aTraAS24qiPCz6gkmYGC8yr6mglcnNoYbsLNYZ69zF1XH +cXPduCPdPdfLlzVlKK1/U7hkA28eG3BIAMh6uJYBRJTpiGgaGdPd7YekUB8S6cy+ +CQIDAQAB +-----END PUBLIC KEY----- +""") + +class ExpectedError(Exception): + """A novice-readable exception that also carries the original exception for + debugging""" + + +class HttpsGetter(object): + def __init__(self): + """Build an HTTPS opener.""" + # Based on pip 1.4.1's URLOpener + # This verifies certs on only Python >=2.7.9. + self._opener = build_opener(HTTPSHandler()) + # Strip out HTTPHandler to prevent MITM spoof: + for handler in self._opener.handlers: + if isinstance(handler, HTTPHandler): + self._opener.handlers.remove(handler) + + def get(self, url): + """Return the document contents pointed to by an HTTPS URL. + + If something goes wrong (404, timeout, etc.), raise ExpectedError. + + """ + try: + return self._opener.open(url).read() + except (HTTPError, IOError) as exc: + raise ExpectedError("Couldn't download %s." % url, exc) + + +def write(contents, dir, filename): + """Write something to a file in a certain directory.""" + with open(join(dir, filename), 'w') as file: + file.write(contents) + + +def latest_stable_version(get): + """Return the latest stable release of letsencrypt.""" + metadata = loads(get( + environ.get('LE_AUTO_JSON_URL', + 'https://pypi.python.org/pypi/letsencrypt/json'))) + # metadata['info']['version'] actually returns the latest of any kind of + # release release, contrary to https://wiki.python.org/moin/PyPIJSON. + # The regex is a sufficient regex for picking out prereleases for most + # packages, LE included. + return str(max(LooseVersion(r) for r + in metadata['releases'].iterkeys() + if re.match('^[0-9.]+$', r))) + + +def verified_new_le_auto(get, tag, temp_dir): + """Return the path to a verified, up-to-date letsencrypt-auto script. + + If the download's signature does not verify or something else goes wrong + with the verification process, raise ExpectedError. + + """ + le_auto_dir = environ.get( + 'LE_AUTO_DIR_TEMPLATE', + 'https://raw.githubusercontent.com/letsencrypt/letsencrypt/%s/' + 'letsencrypt-auto-source/') % tag + write(get(le_auto_dir + 'letsencrypt-auto'), temp_dir, 'letsencrypt-auto') + write(get(le_auto_dir + 'letsencrypt-auto.sig'), temp_dir, 'letsencrypt-auto.sig') + write(PUBLIC_KEY, temp_dir, 'public_key.pem') + try: + with open(devnull, 'w') as dev_null: + check_call(['openssl', 'dgst', '-sha256', '-verify', + join(temp_dir, 'public_key.pem'), + '-signature', + join(temp_dir, 'letsencrypt-auto.sig'), + join(temp_dir, 'letsencrypt-auto')], + stdout=dev_null, + stderr=dev_null) + except CalledProcessError as exc: + raise ExpectedError("Couldn't verify signature of downloaded " + "certbot-auto.", exc) + + +def main(): + get = HttpsGetter().get + flag = argv[1] + try: + if flag == '--latest-version': + print latest_stable_version(get) + elif flag == '--le-auto-script': + tag = argv[2] + verified_new_le_auto(get, tag, dirname(argv[0])) + except ExpectedError as exc: + print exc.args[0], exc.args[1] + return 1 + else: + return 0 + + +if __name__ == '__main__': + exit(main()) + +UNLIKELY_EOF + # --------------------------------------------------------------------------- + DeterminePythonVersion + if ! REMOTE_VERSION=`"$LE_PYTHON" "$TEMP_DIR/fetch.py" --latest-version` ; then + echo "WARNING: unable to check for updates." + elif [ "$LE_AUTO_VERSION" != "$REMOTE_VERSION" ]; then + echo "Upgrading certbot-auto $LE_AUTO_VERSION to $REMOTE_VERSION..." + + # Now we drop into Python so we don't have to install even more + # dependencies (curl, etc.), for better flow control, and for the option of + # future Windows compatibility. + "$LE_PYTHON" "$TEMP_DIR/fetch.py" --le-auto-script "v$REMOTE_VERSION" + + # Install new copy of certbot-auto. + # TODO: Deal with quotes in pathnames. + echo "Replacing certbot-auto..." + # Clone permissions with cp. chmod and chown don't have a --reference + # option on OS X or BSD, and stat -c on Linux is stat -f on OS X and BSD: + $SUDO cp -p "$0" "$TEMP_DIR/letsencrypt-auto.permission-clone" + $SUDO cp "$TEMP_DIR/letsencrypt-auto" "$TEMP_DIR/letsencrypt-auto.permission-clone" + # Using mv rather than cp leaves the old file descriptor pointing to the + # original copy so the shell can continue to read it unmolested. mv across + # filesystems is non-atomic, doing `rm dest, cp src dest, rm src`, but the + # cp is unlikely to fail (esp. under sudo) if the rm doesn't. + $SUDO mv -f "$TEMP_DIR/letsencrypt-auto.permission-clone" "$0" + # TODO: Clean up temp dir safely, even if it has quotes in its path. + rm -rf "$TEMP_DIR" + fi # A newer version is available. + fi # Self-upgrading is allowed. + + "$0" --le-auto-phase2 "$@" +fi diff --git a/certbot-compatibility-test/setup.py b/certbot-compatibility-test/setup.py index c62a10f89..ee2e128dd 100644 --- a/certbot-compatibility-test/setup.py +++ b/certbot-compatibility-test/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.6.0.dev0' +version = '0.6.0' install_requires = [ 'certbot=={0}'.format(version), diff --git a/certbot-nginx/setup.py b/certbot-nginx/setup.py index 0e5c27a0a..634092fff 100644 --- a/certbot-nginx/setup.py +++ b/certbot-nginx/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.6.0.dev0' +version = '0.6.0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot/__init__.py b/certbot/__init__.py index a48d62548..84022010b 100644 --- a/certbot/__init__.py +++ b/certbot/__init__.py @@ -1,4 +1,4 @@ """Certbot client.""" # version number like 1.2.3a0, must have at least 2 parts, like 1.2 -__version__ = '0.6.0.dev0' +__version__ = '0.6.0' diff --git a/letsencrypt-apache/setup.py b/letsencrypt-apache/setup.py index a52044f87..1718cc5b4 100644 --- a/letsencrypt-apache/setup.py +++ b/letsencrypt-apache/setup.py @@ -16,7 +16,7 @@ here = os.path.abspath(os.path.dirname(__file__)) readme = read_file(os.path.join(here, 'README.rst')) -version = '0.6.0.dev0' +version = '0.6.0' # This package is a simple shim around certbot-apache diff --git a/letsencrypt-auto b/letsencrypt-auto index 942fd8ea2..8c6e6c486 100755 --- a/letsencrypt-auto +++ b/letsencrypt-auto @@ -1,6 +1,6 @@ #!/bin/sh # -# Download and run the latest release version of the Let's Encrypt client. +# Download and run the latest release version of the Certbot client. # # NOTE: THIS SCRIPT IS AUTO-GENERATED AND SELF-UPDATING # @@ -19,11 +19,36 @@ XDG_DATA_HOME=${XDG_DATA_HOME:-~/.local/share} VENV_NAME="letsencrypt" VENV_PATH=${VENV_PATH:-"$XDG_DATA_HOME/$VENV_NAME"} VENV_BIN="$VENV_PATH/bin" -LE_AUTO_VERSION="0.5.0" +LE_AUTO_VERSION="0.6.0" +BASENAME=$(basename $0) +USAGE="Usage: $BASENAME [OPTIONS] +A self-updating wrapper script for the Certbot ACME client. When run, updates +to both this script and certbot will be downloaded and installed. After +ensuring you have the latest versions installed, certbot will be invoked with +all arguments you have provided. + +Help for certbot itself cannot be provided until it is installed. + + --debug attempt experimental installation + -h, --help print this help + -n, --non-interactive, --noninteractive run without asking for user input + --no-self-upgrade do not download updates + --os-packages-only install OS dependencies and exit + -v, --verbose provide more output + +All arguments are accepted and forwarded to the Certbot client when run." + +while getopts ":hnv" arg; do + case $arg in + h) + HELP=1;; + n) + ASSUME_YES=1;; + v) + VERBOSE=1;; + esac +done -# This script takes the same arguments as the main letsencrypt program, but it -# additionally responds to --verbose (more output) and --debug (allow support -# for experimental platforms) for arg in "$@" ; do case "$arg" in --debug) @@ -34,25 +59,26 @@ for arg in "$@" ; do # Do not upgrade this script (also prevents client upgrades, because each # copy of the script pins a hash of the python client) NO_SELF_UPGRADE=1;; + --help) + HELP=1;; + --noninteractive|--non-interactive) + ASSUME_YES=1;; --verbose) VERBOSE=1;; - [!-]*|-*[!v]*|-) - # Anything that isn't -v, -vv, etc.: that is, anything that does not - # start with a -, contains anything that's not a v, or is just "-" - ;; - *) # -v+ remains. - VERBOSE=1;; esac done -# letsencrypt-auto needs root access to bootstrap OS dependencies, and -# letsencrypt itself needs root access for almost all modes of operation +# certbot-auto needs root access to bootstrap OS dependencies, and +# certbot itself needs root access for almost all modes of operation # The "normal" case is that sudo is used for the steps that need root, but # this script *can* be run as root (not recommended), or fall back to using # `su` +SUDO_ENV="" +export CERTBOT_AUTO="$0" if test "`id -u`" -ne "0" ; then if command -v sudo 1>/dev/null 2>&1; then SUDO=sudo + SUDO_ENV="CERTBOT_AUTO=$0" else echo \"sudo\" is not available, will use \"su\" for installation steps... # Because the parameters in `su -c` has to be a string, @@ -81,6 +107,12 @@ else SUDO= fi +if [ $BASENAME = "letsencrypt-auto" ]; then + # letsencrypt-auto does not respect --help or --yes for backwards compatibility + ASSUME_YES=1 + HELP=0 +fi + ExperimentalBootstrap() { # Arguments: Platform name, bootstrap function name if [ "$DEBUG" = 1 ]; then @@ -151,30 +183,45 @@ BootstrapDebCommon() { augeas_pkg="libaugeas0 augeas-lenses" AUGVERSION=`apt-cache show --no-all-versions libaugeas0 | grep ^Version: | cut -d" " -f2` + if [ "$ASSUME_YES" = 1 ]; then + YES_FLAG="-y" + fi + AddBackportRepo() { # ARGS: BACKPORT_NAME="$1" BACKPORT_SOURCELINE="$2" + echo "To use the Apache Certbot plugin, augeas needs to be installed from $BACKPORT_NAME." if ! grep -v -e ' *#' /etc/apt/sources.list | grep -q "$BACKPORT_NAME" ; then # This can theoretically error if sources.list.d is empty, but in that case we don't care. if ! grep -v -e ' *#' /etc/apt/sources.list.d/* 2>/dev/null | grep -q "$BACKPORT_NAME"; then - /bin/echo -n "Installing augeas from $BACKPORT_NAME in 3 seconds..." - sleep 1s - /bin/echo -ne "\e[0K\rInstalling augeas from $BACKPORT_NAME in 2 seconds..." - sleep 1s - /bin/echo -e "\e[0K\rInstalling augeas from $BACKPORT_NAME in 1 second ..." - sleep 1s - if echo $BACKPORT_NAME | grep -q wheezy ; then - /bin/echo '(Backports are only installed if explicitly requested via "apt-get install -t wheezy-backports")' + if [ "$ASSUME_YES" = 1 ]; then + /bin/echo -n "Installing augeas from $BACKPORT_NAME in 3 seconds..." + sleep 1s + /bin/echo -ne "\e[0K\rInstalling augeas from $BACKPORT_NAME in 2 seconds..." + sleep 1s + /bin/echo -e "\e[0K\rInstalling augeas from $BACKPORT_NAME in 1 second ..." + sleep 1s + add_backports=1 + else + read -p "Would you like to enable the $BACKPORT_NAME repository [Y/n]? " response + case $response in + [yY][eE][sS]|[yY]|"") + add_backports=1;; + *) + add_backports=0;; + esac + fi + if [ "$add_backports" = 1 ]; then + $SUDO sh -c "echo $BACKPORT_SOURCELINE >> /etc/apt/sources.list.d/$BACKPORT_NAME.list" + $SUDO apt-get update fi - - $SUDO sh -c "echo $BACKPORT_SOURCELINE >> /etc/apt/sources.list.d/$BACKPORT_NAME.list" - $SUDO apt-get update fi fi - $SUDO apt-get install -y --no-install-recommends -t "$BACKPORT_NAME" $augeas_pkg - augeas_pkg= - + if [ "$add_backports" != 0 ]; then + $SUDO apt-get install $YES_FLAG --no-install-recommends -t "$BACKPORT_NAME" $augeas_pkg + augeas_pkg= + fi } @@ -186,12 +233,12 @@ BootstrapDebCommon() { AddBackportRepo precise-backports "deb http://archive.ubuntu.com/ubuntu precise-backports main restricted universe multiverse" else echo "No libaugeas0 version is available that's new enough to run the" - echo "Let's Encrypt apache plugin..." + echo "Certbot apache plugin..." fi # XXX add a case for ubuntu PPAs fi - $SUDO apt-get install -y --no-install-recommends \ + $SUDO apt-get install $YES_FLAG --no-install-recommends \ python \ python-dev \ $virtualenv \ @@ -212,9 +259,10 @@ BootstrapDebCommon() { BootstrapRpmCommon() { # Tested with: - # - Fedora 22, 23 (x64) + # - Fedora 20, 21, 22, 23 (x64) # - Centos 7 (x64: on DigitalOcean droplet) # - CentOS 7 Minimal install in a Hyper-V VM + # - CentOS 6 (EPEL must be installed manually) if type dnf 2>/dev/null then @@ -228,54 +276,62 @@ BootstrapRpmCommon() { exit 1 fi + pkgs=" + gcc + dialog + augeas-libs + openssl + openssl-devel + libffi-devel + redhat-rpm-config + ca-certificates + " + # Some distros and older versions of current distros use a "python27" # instead of "python" naming convention. Try both conventions. - if ! $SUDO $tool install -y \ - python \ - python-devel \ - python-virtualenv \ - python-tools \ - python-pip - then - if ! $SUDO $tool install -y \ - python27 \ - python27-devel \ - python27-virtualenv \ - python27-tools \ - python27-pip - then - echo "Could not install Python dependencies. Aborting bootstrap!" - exit 1 - fi + if $SUDO $tool list python >/dev/null 2>&1; then + pkgs="$pkgs + python + python-devel + python-virtualenv + python-tools + python-pip + " + else + pkgs="$pkgs + python27 + python27-devel + python27-virtualenv + python27-tools + python27-pip + " fi - if ! $SUDO $tool install -y \ - gcc \ - dialog \ - augeas-libs \ - openssl \ - openssl-devel \ - libffi-devel \ - redhat-rpm-config \ - ca-certificates - then - echo "Could not install additional dependencies. Aborting bootstrap!" - exit 1 - fi - - if $SUDO $tool list installed "httpd" >/dev/null 2>&1; then - if ! $SUDO $tool install -y mod_ssl - then - echo "Apache found, but mod_ssl could not be installed." - fi + pkgs="$pkgs + mod_ssl + " + fi + + if [ "$ASSUME_YES" = 1 ]; then + yes_flag="-y" + fi + + if ! $SUDO $tool install $yes_flag $pkgs; then + echo "Could not install OS dependencies. Aborting bootstrap!" + exit 1 fi } BootstrapSuseCommon() { # SLE12 don't have python-virtualenv - $SUDO zypper -nq in -l \ + if [ "$ASSUME_YES" = 1 ]; then + zypper_flags="-nq" + install_flags="-l" + fi + + $SUDO zypper $zypper_flags in $install_flags \ python \ python-devel \ python-virtualenv \ @@ -310,8 +366,12 @@ BootstrapArchCommon() { # pacman -T exits with 127 if there are missing dependencies missing=$($SUDO pacman -T $deps) || true + if [ "$ASSUME_YES" = 1 ]; then + noconfirm="--noconfirm" + fi + if [ "$missing" ]; then - $SUDO pacman -S --needed $missing + $SUDO pacman -S --needed $missing $noconfirm fi } @@ -426,7 +486,7 @@ Bootstrap() { elif grep -iq "Amazon Linux" /etc/issue ; then ExperimentalBootstrap "Amazon Linux" BootstrapRpmCommon else - echo "Sorry, I don't know how to bootstrap Let's Encrypt on your operating system!" + echo "Sorry, I don't know how to bootstrap Certbot on your operating system!" echo echo "You will need to bootstrap, configure virtualenv, and run pip install manually." echo "Please see https://letsencrypt.readthedocs.org/en/latest/contributing.html#prerequisites" @@ -446,7 +506,8 @@ if [ "$1" = "--le-auto-phase2" ]; then shift 1 # the --le-auto-phase2 arg if [ -f "$VENV_BIN/letsencrypt" ]; then # --version output ran through grep due to python-cryptography DeprecationWarnings - INSTALLED_VERSION=$("$VENV_BIN/letsencrypt" --version 2>&1 | grep ^letsencrypt | cut -d " " -f 2) + # grep for both certbot and letsencrypt until certbot and shim packages have been released + INSTALLED_VERSION=$("$VENV_BIN/letsencrypt" --version 2>&1 | grep "^certbot\|^letsencrypt" | cut -d " " -f 2) else INSTALLED_VERSION="none" fi @@ -465,8 +526,8 @@ if [ "$1" = "--le-auto-phase2" ]; then # There is no $ interpolation due to quotes on starting heredoc delimiter. # ------------------------------------------------------------------------- cat << "UNLIKELY_EOF" > "$TEMP_DIR/letsencrypt-auto-requirements.txt" -# This is the flattened list of packages letsencrypt-auto installs. To generate -# this, do `pip install --no-cache-dir -e acme -e . -e letsencrypt-apache`, and +# This is the flattened list of packages certbot-auto installs. To generate +# this, do `pip install --no-cache-dir -e acme -e . -e certbot-apache`, and # then use `hashin` or a more secure method to gather the hashes. argparse==1.4.0 \ @@ -645,15 +706,21 @@ mock==1.0.1 \ # THE LINES BELOW ARE EDITED BY THE RELEASE SCRIPT; ADD ALL DEPENDENCIES ABOVE. -acme==0.5.0 \ - --hash=sha256:ceb4127c13213f0006a564be82176b968c6b374d20d9fc78555d0658a252b275 \ - --hash=sha256:0605c63c656d33c883a05675f5db9cfb85d503f2771c885031800e0da7631abd -letsencrypt==0.5.0 \ - --hash=sha256:f90f883e99cdbdf8142335bdbf4f74a8af143ee4b4ec60fb49c6e47418c1114c \ - --hash=sha256:e38a2b70b82be79bc195307652244a3e012ec73d897d4dbd3f80cf698496d15a -letsencrypt-apache==0.5.0 \ - --hash=sha256:a767882164a7b09d9c12c80684a28a782135fdaf35654ef5a02c0b7b1d27ab8d \ - --hash=sha256:c20e7b9c517aa4a7d70e6bd9382da7259f00bc191b9e60d8e312e48837a00c41 +acme==0.6.0 \ + --hash=sha256:cbe4e7a340a19725a8740ed86e30abdbe18fc22c4c6022b7a8e56642d502bcc3 \ + --hash=sha256:ec4e6009dfbd629b58473eb06bbebfd9fb2a79fc8831c149e9205bc38a98ecc6 +certbot==0.6.0 \ + --hash=sha256:a893632d228864b0a751db9f3fdd93439ed34b988ea21b64fb0f0fa2ceded6a2 \ + --hash=sha256:80b0b7dc5afeec2816ef638a61e7c628d73cd72666eebf4984be426d1c2b492d +certbot-apache==0.6.0 \ + --hash=sha256:0ab077f0913b81ed5c1b141c3a7c4c0228ef3738d8d61a93db794d9a80718d43 \ + --hash=sha256:1cfbe751209079a803758f472200816fac559f2a36fdd582d25e3ba5601423a1 +letsencrypt==0.6.0 \ + --hash=sha256:93196c7dcd57272a753e525d145c5a9987c8968c22ec954bcf83dcc9d2499a76 \ + --hash=sha256:a16d6c395f1bf5fd61a28ef83dc78f42dbecbad9d00be6236f2ad8915645c154 +letsencrypt-apache==0.6.0 \ + --hash=sha256:02fadc52a0796e53978c508beec9c53e1fc047660240832b9bde5d53ab3a1379 \ + --hash=sha256:1c5522d94d7750bdb9bfa6201d2c263e914f662c9d0079e673167233cf4364f1 UNLIKELY_EOF # ------------------------------------------------------------------------- @@ -823,18 +890,30 @@ UNLIKELY_EOF fi echo "Installation succeeded." fi - echo "Requesting root privileges to run letsencrypt..." - echo " " $SUDO "$VENV_BIN/letsencrypt" "$@" - $SUDO "$VENV_BIN/letsencrypt" "$@" + echo "Requesting root privileges to run certbot..." + if [ -z "$SUDO_ENV" ] ; then + # SUDO is su wrapper / noop + echo " " $SUDO "$VENV_BIN/letsencrypt" "$@" + $SUDO "$VENV_BIN/letsencrypt" "$@" + else + # sudo + echo " " $SUDO "$SUDO_ENV" "$VENV_BIN/letsencrypt" "$@" + $SUDO "$SUDO_ENV" "$VENV_BIN/letsencrypt" "$@" + fi + else - # Phase 1: Upgrade letsencrypt-auto if neceesary, then self-invoke. + # Phase 1: Upgrade certbot-auto if neceesary, then self-invoke. # # Each phase checks the version of only the thing it is responsible for # upgrading. Phase 1 checks the version of the latest release of - # letsencrypt-auto (which is always the same as that of the letsencrypt - # package). Phase 2 checks the version of the locally installed letsencrypt. + # certbot-auto (which is always the same as that of the certbot + # package). Phase 2 checks the version of the locally installed certbot. if [ ! -f "$VENV_BIN/letsencrypt" ]; then + if [ "$HELP" = 1 ]; then + echo "$USAGE" + exit 0 + fi # If it looks like we've never bootstrapped before, bootstrap: Bootstrap fi @@ -953,7 +1032,7 @@ def verified_new_le_auto(get, tag, temp_dir): stderr=dev_null) except CalledProcessError as exc: raise ExpectedError("Couldn't verify signature of downloaded " - "letsencrypt-auto.", exc) + "certbot-auto.", exc) def main(): @@ -978,29 +1057,27 @@ if __name__ == '__main__': UNLIKELY_EOF # --------------------------------------------------------------------------- DeterminePythonVersion - REMOTE_VERSION=`"$LE_PYTHON" "$TEMP_DIR/fetch.py" --latest-version` - if [ "$LE_AUTO_VERSION" != "$REMOTE_VERSION" ]; then - echo "Upgrading letsencrypt-auto $LE_AUTO_VERSION to $REMOTE_VERSION..." + if ! REMOTE_VERSION=`"$LE_PYTHON" "$TEMP_DIR/fetch.py" --latest-version` ; then + echo "WARNING: unable to check for updates." + elif [ "$LE_AUTO_VERSION" != "$REMOTE_VERSION" ]; then + echo "Upgrading certbot-auto $LE_AUTO_VERSION to $REMOTE_VERSION..." # Now we drop into Python so we don't have to install even more # dependencies (curl, etc.), for better flow control, and for the option of # future Windows compatibility. "$LE_PYTHON" "$TEMP_DIR/fetch.py" --le-auto-script "v$REMOTE_VERSION" - # Install new copy of letsencrypt-auto. + # Install new copy of certbot-auto. # TODO: Deal with quotes in pathnames. - echo "Replacing letsencrypt-auto..." + echo "Replacing certbot-auto..." # Clone permissions with cp. chmod and chown don't have a --reference # option on OS X or BSD, and stat -c on Linux is stat -f on OS X and BSD: - echo " " $SUDO cp -p "$0" "$TEMP_DIR/letsencrypt-auto.permission-clone" $SUDO cp -p "$0" "$TEMP_DIR/letsencrypt-auto.permission-clone" - echo " " $SUDO cp "$TEMP_DIR/letsencrypt-auto" "$TEMP_DIR/letsencrypt-auto.permission-clone" $SUDO cp "$TEMP_DIR/letsencrypt-auto" "$TEMP_DIR/letsencrypt-auto.permission-clone" # Using mv rather than cp leaves the old file descriptor pointing to the # original copy so the shell can continue to read it unmolested. mv across # filesystems is non-atomic, doing `rm dest, cp src dest, rm src`, but the # cp is unlikely to fail (esp. under sudo) if the rm doesn't. - echo " " $SUDO mv -f "$TEMP_DIR/letsencrypt-auto.permission-clone" "$0" $SUDO mv -f "$TEMP_DIR/letsencrypt-auto.permission-clone" "$0" # TODO: Clean up temp dir safely, even if it has quotes in its path. rm -rf "$TEMP_DIR" diff --git a/letsencrypt-auto-source/certbot-auto.asc b/letsencrypt-auto-source/certbot-auto.asc new file mode 100644 index 000000000..8b4f34c70 --- /dev/null +++ b/letsencrypt-auto-source/certbot-auto.asc @@ -0,0 +1,11 @@ +-----BEGIN PGP SIGNATURE----- +Version: GnuPG v1 + +iQEcBAABAgAGBQJXM9ZDAAoJEE0XyZXNl3XyzGkH/2KeR0jYxXKlvwfCkxU6hSC0 +eXcxZVQk59hCSvkNGE6Mj6rwQcyjSqmRp14MaJpq7NZADN6F+HWb6VB/Wq6moMQs +PJtthqwhF767Qg+Py9Hp6XmlKscjXB6AKCVxq5TBwEIOTtj0rhQRLF9/+GW6jFuf +kT6aUcDWNjOyWWUtp9vOVprDtegrltp0/2DNitlvPu263pKC+7I3GyLTq4fKP4EE +auZSAhFry9SNR3Usf2wD3kzhvLSrT3h9Yh5oA04oaX9H6e86EHwt6RJJRHpg8s6b +e0CBIIuaRJEmdiMUWlV/gAfH6M2PbG1wtJdxc0ThNEoWAjTsopr61BoHJ3cpCy4= +=+e7/ +-----END PGP SIGNATURE----- diff --git a/letsencrypt-auto-source/letsencrypt-auto b/letsencrypt-auto-source/letsencrypt-auto index 8578feef2..8c6e6c486 100755 --- a/letsencrypt-auto-source/letsencrypt-auto +++ b/letsencrypt-auto-source/letsencrypt-auto @@ -19,7 +19,7 @@ XDG_DATA_HOME=${XDG_DATA_HOME:-~/.local/share} VENV_NAME="letsencrypt" VENV_PATH=${VENV_PATH:-"$XDG_DATA_HOME/$VENV_NAME"} VENV_BIN="$VENV_PATH/bin" -LE_AUTO_VERSION="0.6.0.dev0" +LE_AUTO_VERSION="0.6.0" BASENAME=$(basename $0) USAGE="Usage: $BASENAME [OPTIONS] A self-updating wrapper script for the Certbot ACME client. When run, updates @@ -706,15 +706,21 @@ mock==1.0.1 \ # THE LINES BELOW ARE EDITED BY THE RELEASE SCRIPT; ADD ALL DEPENDENCIES ABOVE. -acme==0.5.0 \ - --hash=sha256:ceb4127c13213f0006a564be82176b968c6b374d20d9fc78555d0658a252b275 \ - --hash=sha256:0605c63c656d33c883a05675f5db9cfb85d503f2771c885031800e0da7631abd -letsencrypt==0.5.0 \ - --hash=sha256:f90f883e99cdbdf8142335bdbf4f74a8af143ee4b4ec60fb49c6e47418c1114c \ - --hash=sha256:e38a2b70b82be79bc195307652244a3e012ec73d897d4dbd3f80cf698496d15a -letsencrypt-apache==0.5.0 \ - --hash=sha256:a767882164a7b09d9c12c80684a28a782135fdaf35654ef5a02c0b7b1d27ab8d \ - --hash=sha256:c20e7b9c517aa4a7d70e6bd9382da7259f00bc191b9e60d8e312e48837a00c41 +acme==0.6.0 \ + --hash=sha256:cbe4e7a340a19725a8740ed86e30abdbe18fc22c4c6022b7a8e56642d502bcc3 \ + --hash=sha256:ec4e6009dfbd629b58473eb06bbebfd9fb2a79fc8831c149e9205bc38a98ecc6 +certbot==0.6.0 \ + --hash=sha256:a893632d228864b0a751db9f3fdd93439ed34b988ea21b64fb0f0fa2ceded6a2 \ + --hash=sha256:80b0b7dc5afeec2816ef638a61e7c628d73cd72666eebf4984be426d1c2b492d +certbot-apache==0.6.0 \ + --hash=sha256:0ab077f0913b81ed5c1b141c3a7c4c0228ef3738d8d61a93db794d9a80718d43 \ + --hash=sha256:1cfbe751209079a803758f472200816fac559f2a36fdd582d25e3ba5601423a1 +letsencrypt==0.6.0 \ + --hash=sha256:93196c7dcd57272a753e525d145c5a9987c8968c22ec954bcf83dcc9d2499a76 \ + --hash=sha256:a16d6c395f1bf5fd61a28ef83dc78f42dbecbad9d00be6236f2ad8915645c154 +letsencrypt-apache==0.6.0 \ + --hash=sha256:02fadc52a0796e53978c508beec9c53e1fc047660240832b9bde5d53ab3a1379 \ + --hash=sha256:1c5522d94d7750bdb9bfa6201d2c263e914f662c9d0079e673167233cf4364f1 UNLIKELY_EOF # ------------------------------------------------------------------------- diff --git a/letsencrypt-auto-source/letsencrypt-auto.sig b/letsencrypt-auto-source/letsencrypt-auto.sig index 36ab206aa47ee8d6348340e2e17e66ef840f4832..cb360f89ef5443af2c6bc157704a98d3cd68fcef 100644 GIT binary patch literal 256 zcmV+b0ssCk`Do-WYWlUAhmM)Jao($YX$9Q(cRVv8_jtP0o}fz5%~*%F!qFPG{FHTs zO#Q7Ny@?Hph=ylMIohpY9^{Zmm8Bm3`9U?K3CDJ~iXxyHmSNf`Uq7cCyf|^Mc+2uA z+A^~xvGqM}C5Xzw8cyZq9H=SydJxxr0AB GYGOMK)`FS< literal 256 zcmV+b0ssCNAlb`yp8ViHw_s6WQdeQ~rmeR1#f`~|;Lcvp1i>?dOn)2S?|UXU3-X4k zaZ%*t!KjDe7yUlErpaEi6!y=8HI`DRz)EuS7bCS?0nWEvmfo;6rg zd*P;)T`MMf6qh)Nq~anD`EcT+-{;e}X{!NGWzN5f-uFVW&D40h!-VgVXfn*mr{?;Y4vJd_;=@v>g=fCE0&HR8N!p!aAH z8#Erxi1sEj2Zdym%<&YjXJ9%;%Y|e9?#~icJ0k#V&W79xyG>~1A}+~ diff --git a/letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt b/letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt index 3a1ce34f1..f4ceae536 100644 --- a/letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt +++ b/letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt @@ -178,12 +178,18 @@ mock==1.0.1 \ # THE LINES BELOW ARE EDITED BY THE RELEASE SCRIPT; ADD ALL DEPENDENCIES ABOVE. -acme==0.5.0 \ - --hash=sha256:ceb4127c13213f0006a564be82176b968c6b374d20d9fc78555d0658a252b275 \ - --hash=sha256:0605c63c656d33c883a05675f5db9cfb85d503f2771c885031800e0da7631abd -letsencrypt==0.5.0 \ - --hash=sha256:f90f883e99cdbdf8142335bdbf4f74a8af143ee4b4ec60fb49c6e47418c1114c \ - --hash=sha256:e38a2b70b82be79bc195307652244a3e012ec73d897d4dbd3f80cf698496d15a -letsencrypt-apache==0.5.0 \ - --hash=sha256:a767882164a7b09d9c12c80684a28a782135fdaf35654ef5a02c0b7b1d27ab8d \ - --hash=sha256:c20e7b9c517aa4a7d70e6bd9382da7259f00bc191b9e60d8e312e48837a00c41 +acme==0.6.0 \ + --hash=sha256:cbe4e7a340a19725a8740ed86e30abdbe18fc22c4c6022b7a8e56642d502bcc3 \ + --hash=sha256:ec4e6009dfbd629b58473eb06bbebfd9fb2a79fc8831c149e9205bc38a98ecc6 +certbot==0.6.0 \ + --hash=sha256:a893632d228864b0a751db9f3fdd93439ed34b988ea21b64fb0f0fa2ceded6a2 \ + --hash=sha256:80b0b7dc5afeec2816ef638a61e7c628d73cd72666eebf4984be426d1c2b492d +certbot-apache==0.6.0 \ + --hash=sha256:0ab077f0913b81ed5c1b141c3a7c4c0228ef3738d8d61a93db794d9a80718d43 \ + --hash=sha256:1cfbe751209079a803758f472200816fac559f2a36fdd582d25e3ba5601423a1 +letsencrypt==0.6.0 \ + --hash=sha256:93196c7dcd57272a753e525d145c5a9987c8968c22ec954bcf83dcc9d2499a76 \ + --hash=sha256:a16d6c395f1bf5fd61a28ef83dc78f42dbecbad9d00be6236f2ad8915645c154 +letsencrypt-apache==0.6.0 \ + --hash=sha256:02fadc52a0796e53978c508beec9c53e1fc047660240832b9bde5d53ab3a1379 \ + --hash=sha256:1c5522d94d7750bdb9bfa6201d2c263e914f662c9d0079e673167233cf4364f1 diff --git a/letsencrypt-nginx/setup.py b/letsencrypt-nginx/setup.py index b94b7f69f..12578cc18 100644 --- a/letsencrypt-nginx/setup.py +++ b/letsencrypt-nginx/setup.py @@ -16,7 +16,7 @@ here = os.path.abspath(os.path.dirname(__file__)) readme = read_file(os.path.join(here, 'README.rst')) -version = '0.6.0.dev0' +version = '0.6.0' # This package is a simple shim around certbot-nginx diff --git a/letsencrypt/setup.py b/letsencrypt/setup.py index 708c31f4b..d30146621 100644 --- a/letsencrypt/setup.py +++ b/letsencrypt/setup.py @@ -20,7 +20,7 @@ readme = read_file(os.path.join(here, 'README.rst')) install_requires = ['certbot'] -version = '0.6.0.dev0' +version = '0.6.0' setup( diff --git a/letshelp-certbot/setup.py b/letshelp-certbot/setup.py index 8359d2766..8c302472a 100644 --- a/letshelp-certbot/setup.py +++ b/letshelp-certbot/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.6.0.dev0' +version = '0.6.0' install_requires = [ 'setuptools', # pkg_resources diff --git a/letshelp-letsencrypt/setup.py b/letshelp-letsencrypt/setup.py index 875c6fc92..bc4141513 100644 --- a/letshelp-letsencrypt/setup.py +++ b/letshelp-letsencrypt/setup.py @@ -16,7 +16,7 @@ here = os.path.abspath(os.path.dirname(__file__)) readme = read_file(os.path.join(here, 'README.rst')) -version = '0.6.0.dev0' +version = '0.6.0' # This package is a simple shim around letshelp-certbot From c8cf0b460045c9853136d74772c4235817c1d1e4 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 11 May 2016 18:04:27 -0700 Subject: [PATCH 092/396] Bump version to 0.7.0 --- acme/setup.py | 2 +- certbot-apache/setup.py | 2 +- certbot-compatibility-test/setup.py | 2 +- certbot-nginx/setup.py | 2 +- certbot/__init__.py | 2 +- letsencrypt-apache/setup.py | 2 +- letsencrypt-auto-source/letsencrypt-auto | 2 +- letsencrypt-nginx/setup.py | 2 +- letsencrypt/setup.py | 2 +- letshelp-certbot/setup.py | 2 +- letshelp-letsencrypt/setup.py | 2 +- 11 files changed, 11 insertions(+), 11 deletions(-) diff --git a/acme/setup.py b/acme/setup.py index 20c7c7226..d38864dc1 100644 --- a/acme/setup.py +++ b/acme/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.6.0' +version = '0.7.0.dev0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot-apache/setup.py b/certbot-apache/setup.py index 538a68139..56c6a451d 100644 --- a/certbot-apache/setup.py +++ b/certbot-apache/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.6.0' +version = '0.7.0.dev0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot-compatibility-test/setup.py b/certbot-compatibility-test/setup.py index ee2e128dd..8f9452c5a 100644 --- a/certbot-compatibility-test/setup.py +++ b/certbot-compatibility-test/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.6.0' +version = '0.7.0.dev0' install_requires = [ 'certbot=={0}'.format(version), diff --git a/certbot-nginx/setup.py b/certbot-nginx/setup.py index 634092fff..a74b93093 100644 --- a/certbot-nginx/setup.py +++ b/certbot-nginx/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.6.0' +version = '0.7.0.dev0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot/__init__.py b/certbot/__init__.py index 84022010b..85f370e7a 100644 --- a/certbot/__init__.py +++ b/certbot/__init__.py @@ -1,4 +1,4 @@ """Certbot client.""" # version number like 1.2.3a0, must have at least 2 parts, like 1.2 -__version__ = '0.6.0' +__version__ = '0.7.0.dev0' diff --git a/letsencrypt-apache/setup.py b/letsencrypt-apache/setup.py index 1718cc5b4..b94746150 100644 --- a/letsencrypt-apache/setup.py +++ b/letsencrypt-apache/setup.py @@ -16,7 +16,7 @@ here = os.path.abspath(os.path.dirname(__file__)) readme = read_file(os.path.join(here, 'README.rst')) -version = '0.6.0' +version = '0.7.0.dev0' # This package is a simple shim around certbot-apache diff --git a/letsencrypt-auto-source/letsencrypt-auto b/letsencrypt-auto-source/letsencrypt-auto index 8c6e6c486..b255a99a7 100755 --- a/letsencrypt-auto-source/letsencrypt-auto +++ b/letsencrypt-auto-source/letsencrypt-auto @@ -19,7 +19,7 @@ XDG_DATA_HOME=${XDG_DATA_HOME:-~/.local/share} VENV_NAME="letsencrypt" VENV_PATH=${VENV_PATH:-"$XDG_DATA_HOME/$VENV_NAME"} VENV_BIN="$VENV_PATH/bin" -LE_AUTO_VERSION="0.6.0" +LE_AUTO_VERSION="0.7.0.dev0" BASENAME=$(basename $0) USAGE="Usage: $BASENAME [OPTIONS] A self-updating wrapper script for the Certbot ACME client. When run, updates diff --git a/letsencrypt-nginx/setup.py b/letsencrypt-nginx/setup.py index 12578cc18..95ffd6cd8 100644 --- a/letsencrypt-nginx/setup.py +++ b/letsencrypt-nginx/setup.py @@ -16,7 +16,7 @@ here = os.path.abspath(os.path.dirname(__file__)) readme = read_file(os.path.join(here, 'README.rst')) -version = '0.6.0' +version = '0.7.0.dev0' # This package is a simple shim around certbot-nginx diff --git a/letsencrypt/setup.py b/letsencrypt/setup.py index d30146621..7c974ea9b 100644 --- a/letsencrypt/setup.py +++ b/letsencrypt/setup.py @@ -20,7 +20,7 @@ readme = read_file(os.path.join(here, 'README.rst')) install_requires = ['certbot'] -version = '0.6.0' +version = '0.7.0.dev0' setup( diff --git a/letshelp-certbot/setup.py b/letshelp-certbot/setup.py index 8c302472a..b616da688 100644 --- a/letshelp-certbot/setup.py +++ b/letshelp-certbot/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.6.0' +version = '0.7.0.dev0' install_requires = [ 'setuptools', # pkg_resources diff --git a/letshelp-letsencrypt/setup.py b/letshelp-letsencrypt/setup.py index bc4141513..10380c49b 100644 --- a/letshelp-letsencrypt/setup.py +++ b/letshelp-letsencrypt/setup.py @@ -16,7 +16,7 @@ here = os.path.abspath(os.path.dirname(__file__)) readme = read_file(os.path.join(here, 'README.rst')) -version = '0.6.0' +version = '0.7.0.dev0' # This package is a simple shim around letshelp-certbot From f53f4dd4913b1dbfd82d34e05927d91cf228d56f Mon Sep 17 00:00:00 2001 From: Noah Swartz Date: Thu, 12 May 2016 09:30:45 -0700 Subject: [PATCH 093/396] fix docs copyright --- docs/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index b2b31ac5d..e387e1eae 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -65,7 +65,7 @@ master_doc = 'index' # General information about the project. project = u'Certbot' -copyright = u'2014-2016, Certbot Project' +copyright = u'2014-2016 - The Certbot software and documentation are licensed under the Apache 2.0 license as described at https://eff.org/cb-license ' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the From 0d5c1638e72d6a9e347cbcdfd1baf58df83fab6e Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Thu, 12 May 2016 10:06:11 -0700 Subject: [PATCH 094/396] add certbot-auto and PGP sig --- certbot-auto | 1011 ++++++++++++++++++++++ letsencrypt-auto-source/certbot-auto.asc | 11 + 2 files changed, 1022 insertions(+) create mode 100755 certbot-auto create mode 100644 letsencrypt-auto-source/certbot-auto.asc diff --git a/certbot-auto b/certbot-auto new file mode 100755 index 000000000..942fd8ea2 --- /dev/null +++ b/certbot-auto @@ -0,0 +1,1011 @@ +#!/bin/sh +# +# Download and run the latest release version of the Let's Encrypt client. +# +# NOTE: THIS SCRIPT IS AUTO-GENERATED AND SELF-UPDATING +# +# IF YOU WANT TO EDIT IT LOCALLY, *ALWAYS* RUN YOUR COPY WITH THE +# "--no-self-upgrade" FLAG +# +# IF YOU WANT TO SEND PULL REQUESTS, THE REAL SOURCE FOR THIS FILE IS +# letsencrypt-auto-source/letsencrypt-auto.template AND +# letsencrypt-auto-source/pieces/bootstrappers/* + +set -e # Work even if somebody does "sh thisscript.sh". + +# Note: you can set XDG_DATA_HOME or VENV_PATH before running this script, +# if you want to change where the virtual environment will be installed +XDG_DATA_HOME=${XDG_DATA_HOME:-~/.local/share} +VENV_NAME="letsencrypt" +VENV_PATH=${VENV_PATH:-"$XDG_DATA_HOME/$VENV_NAME"} +VENV_BIN="$VENV_PATH/bin" +LE_AUTO_VERSION="0.5.0" + +# This script takes the same arguments as the main letsencrypt program, but it +# additionally responds to --verbose (more output) and --debug (allow support +# for experimental platforms) +for arg in "$@" ; do + case "$arg" in + --debug) + DEBUG=1;; + --os-packages-only) + OS_PACKAGES_ONLY=1;; + --no-self-upgrade) + # Do not upgrade this script (also prevents client upgrades, because each + # copy of the script pins a hash of the python client) + NO_SELF_UPGRADE=1;; + --verbose) + VERBOSE=1;; + [!-]*|-*[!v]*|-) + # Anything that isn't -v, -vv, etc.: that is, anything that does not + # start with a -, contains anything that's not a v, or is just "-" + ;; + *) # -v+ remains. + VERBOSE=1;; + esac +done + +# letsencrypt-auto needs root access to bootstrap OS dependencies, and +# letsencrypt itself needs root access for almost all modes of operation +# The "normal" case is that sudo is used for the steps that need root, but +# this script *can* be run as root (not recommended), or fall back to using +# `su` +if test "`id -u`" -ne "0" ; then + if command -v sudo 1>/dev/null 2>&1; then + SUDO=sudo + else + echo \"sudo\" is not available, will use \"su\" for installation steps... + # Because the parameters in `su -c` has to be a string, + # we need properly escape it + su_sudo() { + args="" + # This `while` loop iterates over all parameters given to this function. + # For each parameter, all `'` will be replace by `'"'"'`, and the escaped string + # will be wrapped in a pair of `'`, then appended to `$args` string + # For example, `echo "It's only 1\$\!"` will be escaped to: + # 'echo' 'It'"'"'s only 1$!' + # │ │└┼┘│ + # │ │ │ └── `'s only 1$!'` the literal string + # │ │ └── `\"'\"` is a single quote (as a string) + # │ └── `'It'`, to be concatenated with the strings following it + # └── `echo` wrapped in a pair of `'`, it's totally fine for the shell command itself + while [ $# -ne 0 ]; do + args="$args'$(printf "%s" "$1" | sed -e "s/'/'\"'\"'/g")' " + shift + done + su root -c "$args" + } + SUDO=su_sudo + fi +else + SUDO= +fi + +ExperimentalBootstrap() { + # Arguments: Platform name, bootstrap function name + if [ "$DEBUG" = 1 ]; then + if [ "$2" != "" ]; then + echo "Bootstrapping dependencies via $1..." + $2 + fi + else + echo "WARNING: $1 support is very experimental at present..." + echo "if you would like to work on improving it, please ensure you have backups" + echo "and then run this script again with the --debug flag!" + exit 1 + fi +} + +DeterminePythonVersion() { + for LE_PYTHON in "$LE_PYTHON" python2.7 python27 python2 python; do + # Break (while keeping the LE_PYTHON value) if found. + command -v "$LE_PYTHON" > /dev/null && break + done + if [ "$?" != "0" ]; then + echo "Cannot find any Pythons; please install one!" + exit 1 + fi + export LE_PYTHON + + PYVER=`"$LE_PYTHON" -V 2>&1 | cut -d" " -f 2 | cut -d. -f1,2 | sed 's/\.//'` + if [ "$PYVER" -lt 26 ]; then + echo "You have an ancient version of Python entombed in your operating system..." + echo "This isn't going to work; you'll need at least version 2.6." + exit 1 + fi +} + +BootstrapDebCommon() { + # Current version tested with: + # + # - Ubuntu + # - 14.04 (x64) + # - 15.04 (x64) + # - Debian + # - 7.9 "wheezy" (x64) + # - sid (2015-10-21) (x64) + + # Past versions tested with: + # + # - Debian 8.0 "jessie" (x64) + # - Raspbian 7.8 (armhf) + + # Believed not to work: + # + # - Debian 6.0.10 "squeeze" (x64) + + $SUDO apt-get update || echo apt-get update hit problems but continuing anyway... + + # virtualenv binary can be found in different packages depending on + # distro version (#346) + + virtualenv= + if apt-cache show virtualenv > /dev/null 2>&1; then + virtualenv="virtualenv" + fi + + if apt-cache show python-virtualenv > /dev/null 2>&1; then + virtualenv="$virtualenv python-virtualenv" + fi + + augeas_pkg="libaugeas0 augeas-lenses" + AUGVERSION=`apt-cache show --no-all-versions libaugeas0 | grep ^Version: | cut -d" " -f2` + + AddBackportRepo() { + # ARGS: + BACKPORT_NAME="$1" + BACKPORT_SOURCELINE="$2" + if ! grep -v -e ' *#' /etc/apt/sources.list | grep -q "$BACKPORT_NAME" ; then + # This can theoretically error if sources.list.d is empty, but in that case we don't care. + if ! grep -v -e ' *#' /etc/apt/sources.list.d/* 2>/dev/null | grep -q "$BACKPORT_NAME"; then + /bin/echo -n "Installing augeas from $BACKPORT_NAME in 3 seconds..." + sleep 1s + /bin/echo -ne "\e[0K\rInstalling augeas from $BACKPORT_NAME in 2 seconds..." + sleep 1s + /bin/echo -e "\e[0K\rInstalling augeas from $BACKPORT_NAME in 1 second ..." + sleep 1s + if echo $BACKPORT_NAME | grep -q wheezy ; then + /bin/echo '(Backports are only installed if explicitly requested via "apt-get install -t wheezy-backports")' + fi + + $SUDO sh -c "echo $BACKPORT_SOURCELINE >> /etc/apt/sources.list.d/$BACKPORT_NAME.list" + $SUDO apt-get update + fi + fi + $SUDO apt-get install -y --no-install-recommends -t "$BACKPORT_NAME" $augeas_pkg + augeas_pkg= + + } + + + if dpkg --compare-versions 1.0 gt "$AUGVERSION" ; then + if lsb_release -a | grep -q wheezy ; then + AddBackportRepo wheezy-backports "deb http://http.debian.net/debian wheezy-backports main" + elif lsb_release -a | grep -q precise ; then + # XXX add ARM case + AddBackportRepo precise-backports "deb http://archive.ubuntu.com/ubuntu precise-backports main restricted universe multiverse" + else + echo "No libaugeas0 version is available that's new enough to run the" + echo "Let's Encrypt apache plugin..." + fi + # XXX add a case for ubuntu PPAs + fi + + $SUDO apt-get install -y --no-install-recommends \ + python \ + python-dev \ + $virtualenv \ + gcc \ + dialog \ + $augeas_pkg \ + libssl-dev \ + libffi-dev \ + ca-certificates \ + + + + if ! command -v virtualenv > /dev/null ; then + echo Failed to install a working \"virtualenv\" command, exiting + exit 1 + fi +} + +BootstrapRpmCommon() { + # Tested with: + # - Fedora 22, 23 (x64) + # - Centos 7 (x64: on DigitalOcean droplet) + # - CentOS 7 Minimal install in a Hyper-V VM + + if type dnf 2>/dev/null + then + tool=dnf + elif type yum 2>/dev/null + then + tool=yum + + else + echo "Neither yum nor dnf found. Aborting bootstrap!" + exit 1 + fi + + # Some distros and older versions of current distros use a "python27" + # instead of "python" naming convention. Try both conventions. + if ! $SUDO $tool install -y \ + python \ + python-devel \ + python-virtualenv \ + python-tools \ + python-pip + then + if ! $SUDO $tool install -y \ + python27 \ + python27-devel \ + python27-virtualenv \ + python27-tools \ + python27-pip + then + echo "Could not install Python dependencies. Aborting bootstrap!" + exit 1 + fi + fi + + if ! $SUDO $tool install -y \ + gcc \ + dialog \ + augeas-libs \ + openssl \ + openssl-devel \ + libffi-devel \ + redhat-rpm-config \ + ca-certificates + then + echo "Could not install additional dependencies. Aborting bootstrap!" + exit 1 + fi + + + if $SUDO $tool list installed "httpd" >/dev/null 2>&1; then + if ! $SUDO $tool install -y mod_ssl + then + echo "Apache found, but mod_ssl could not be installed." + fi + fi +} + +BootstrapSuseCommon() { + # SLE12 don't have python-virtualenv + + $SUDO zypper -nq in -l \ + python \ + python-devel \ + python-virtualenv \ + gcc \ + dialog \ + augeas-lenses \ + libopenssl-devel \ + libffi-devel \ + ca-certificates +} + +BootstrapArchCommon() { + # Tested with: + # - ArchLinux (x86_64) + # + # "python-virtualenv" is Python3, but "python2-virtualenv" provides + # only "virtualenv2" binary, not "virtualenv" necessary in + # ./tools/_venv_common.sh + + deps=" + python2 + python-virtualenv + gcc + dialog + augeas + openssl + libffi + ca-certificates + pkg-config + " + + # pacman -T exits with 127 if there are missing dependencies + missing=$($SUDO pacman -T $deps) || true + + if [ "$missing" ]; then + $SUDO pacman -S --needed $missing + fi +} + +BootstrapGentooCommon() { + PACKAGES=" + dev-lang/python:2.7 + dev-python/virtualenv + dev-util/dialog + app-admin/augeas + dev-libs/openssl + dev-libs/libffi + app-misc/ca-certificates + virtual/pkgconfig" + + case "$PACKAGE_MANAGER" in + (paludis) + $SUDO cave resolve --preserve-world --keep-targets if-possible $PACKAGES -x + ;; + (pkgcore) + $SUDO pmerge --noreplace --oneshot $PACKAGES + ;; + (portage|*) + $SUDO emerge --noreplace --oneshot $PACKAGES + ;; + esac +} + +BootstrapFreeBsd() { + $SUDO pkg install -Ay \ + python \ + py27-virtualenv \ + augeas \ + libffi +} + +BootstrapMac() { + if hash brew 2>/dev/null; then + echo "Using Homebrew to install dependencies..." + pkgman=brew + pkgcmd="brew install" + elif hash port 2>/dev/null; then + echo "Using MacPorts to install dependencies..." + pkgman=port + pkgcmd="$SUDO port install" + else + echo "No Homebrew/MacPorts; installing Homebrew..." + ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)" + pkgman=brew + pkgcmd="brew install" + fi + + $pkgcmd augeas + $pkgcmd dialog + if [ "$(which python)" = "/System/Library/Frameworks/Python.framework/Versions/2.7/bin/python" ]; then + # We want to avoid using the system Python because it requires root to use pip. + # python.org, MacPorts or HomeBrew Python installations should all be OK. + echo "Installing python..." + $pkgcmd python + fi + + # Workaround for _dlopen not finding augeas on OS X + if [ "$pkgman" = "port" ] && ! [ -e "/usr/local/lib/libaugeas.dylib" ] && [ -e "/opt/local/lib/libaugeas.dylib" ]; then + echo "Applying augeas workaround" + $SUDO ln -s /opt/local/lib/libaugeas.dylib /usr/local/lib + fi + + if ! hash pip 2>/dev/null; then + echo "pip not installed" + echo "Installing pip..." + curl --silent --show-error --retry 5 https://bootstrap.pypa.io/get-pip.py | python + fi + + if ! hash virtualenv 2>/dev/null; then + echo "virtualenv not installed." + echo "Installing with pip..." + pip install virtualenv + fi +} + + +# Install required OS packages: +Bootstrap() { + if [ -f /etc/debian_version ]; then + echo "Bootstrapping dependencies for Debian-based OSes..." + BootstrapDebCommon + elif [ -f /etc/redhat-release ]; then + echo "Bootstrapping dependencies for RedHat-based OSes..." + BootstrapRpmCommon + elif [ -f /etc/os-release ] && `grep -q openSUSE /etc/os-release` ; then + echo "Bootstrapping dependencies for openSUSE-based OSes..." + BootstrapSuseCommon + elif [ -f /etc/arch-release ]; then + if [ "$DEBUG" = 1 ]; then + echo "Bootstrapping dependencies for Archlinux..." + BootstrapArchCommon + else + echo "Please use pacman to install letsencrypt packages:" + echo "# pacman -S letsencrypt letsencrypt-apache" + echo + echo "If you would like to use the virtualenv way, please run the script again with the" + echo "--debug flag." + exit 1 + fi + elif [ -f /etc/manjaro-release ]; then + ExperimentalBootstrap "Manjaro Linux" BootstrapArchCommon + elif [ -f /etc/gentoo-release ]; then + ExperimentalBootstrap "Gentoo" BootstrapGentooCommon + elif uname | grep -iq FreeBSD ; then + ExperimentalBootstrap "FreeBSD" BootstrapFreeBsd + elif uname | grep -iq Darwin ; then + ExperimentalBootstrap "Mac OS X" BootstrapMac + elif grep -iq "Amazon Linux" /etc/issue ; then + ExperimentalBootstrap "Amazon Linux" BootstrapRpmCommon + else + echo "Sorry, I don't know how to bootstrap Let's Encrypt on your operating system!" + echo + echo "You will need to bootstrap, configure virtualenv, and run pip install manually." + echo "Please see https://letsencrypt.readthedocs.org/en/latest/contributing.html#prerequisites" + echo "for more info." + fi +} + +TempDir() { + mktemp -d 2>/dev/null || mktemp -d -t 'le' # Linux || OS X +} + + + +if [ "$1" = "--le-auto-phase2" ]; then + # Phase 2: Create venv, install LE, and run. + + shift 1 # the --le-auto-phase2 arg + if [ -f "$VENV_BIN/letsencrypt" ]; then + # --version output ran through grep due to python-cryptography DeprecationWarnings + INSTALLED_VERSION=$("$VENV_BIN/letsencrypt" --version 2>&1 | grep ^letsencrypt | cut -d " " -f 2) + else + INSTALLED_VERSION="none" + fi + if [ "$LE_AUTO_VERSION" != "$INSTALLED_VERSION" ]; then + echo "Creating virtual environment..." + DeterminePythonVersion + rm -rf "$VENV_PATH" + if [ "$VERBOSE" = 1 ]; then + virtualenv --no-site-packages --python "$LE_PYTHON" "$VENV_PATH" + else + virtualenv --no-site-packages --python "$LE_PYTHON" "$VENV_PATH" > /dev/null + fi + + echo "Installing Python packages..." + TEMP_DIR=$(TempDir) + # There is no $ interpolation due to quotes on starting heredoc delimiter. + # ------------------------------------------------------------------------- + cat << "UNLIKELY_EOF" > "$TEMP_DIR/letsencrypt-auto-requirements.txt" +# This is the flattened list of packages letsencrypt-auto installs. To generate +# this, do `pip install --no-cache-dir -e acme -e . -e letsencrypt-apache`, and +# then use `hashin` or a more secure method to gather the hashes. + +argparse==1.4.0 \ + --hash=sha256:c31647edb69fd3d465a847ea3157d37bed1f95f19760b11a47aa91c04b666314 \ + --hash=sha256:62b089a55be1d8949cd2bc7e0df0bddb9e028faefc8c32038cc84862aefdd6e4 + +# This comes before cffi because cffi will otherwise install an unchecked +# version via setup_requires. +pycparser==2.14 \ + --hash=sha256:7959b4a74abdc27b312fed1c21e6caf9309ce0b29ea86b591fd2e99ecdf27f73 + +cffi==1.4.2 \ + --hash=sha256:53c1c9ddb30431513eb7f3cdef0a3e06b0f1252188aaa7744af0f5a4cd45dbaf \ + --hash=sha256:a568f49dfca12a8d9f370187257efc58a38109e1eee714d928561d7a018a64f8 \ + --hash=sha256:809c6ca8cfbcaeebfbd432b4576001b40d38ff2463773cb57577d75e1a020bc3 \ + --hash=sha256:86cdca2cd9cba41422230390df17dfeaa9f344a911e3975c8be9da57b35548e9 \ + --hash=sha256:24b13db84aec385ca23c7b8ded83ef8bb4177bc181d14758f9f975be5d020d86 \ + --hash=sha256:969aeffd7c0e097f6be1efd682c156ae226591a0793a94b6c2d5e4293f4c8d4e \ + --hash=sha256:000f358d4b0fa249feaab9c1ce7d5b2fe7e02e7bdf6806c26418505fc685e268 \ + --hash=sha256:a9d86f460bbd8358a2d513ad779e3f3fc878e3b93a00b5002faebf616ffe6b9c \ + --hash=sha256:3127b3ab33eb23ccac071f9a0802748e5cf7c5cbcd02482bb063e35b41dbb0b0 \ + --hash=sha256:e2b2d42236469a40224d39e7b6c60575f388b2f423f354c7ee90a5b7f58c8065 \ + --hash=sha256:8c2dccafee89b1b424b0bec6ad2dd9622c949d2024e929f5da1ed801eac75f1d \ + --hash=sha256:a4de7a4d11aed488bab4fb14f4988587a829bece5a20433f780d6e33b08083cb \ + --hash=sha256:5ca8fe30425265a49274e4b0213a1bc98f4b13449ae5e96f984771e5d83e58c1 \ + --hash=sha256:a4fd38802f59e714eba81a024f62db710b27dbe27a7ea12e911537327aa84d30 \ + --hash=sha256:86cd6912bbc83e9405d4a73cd7f4b4ee8353652d2dbc7c820106ed5b4d1bab3a \ + --hash=sha256:8f1d177d364ea35900415ae24ca3e471be3d5334ed0419294068c49f45913998 +ConfigArgParse==0.10.0 \ + --hash=sha256:3b50a83dd58149dfcee98cb6565265d10b53e9c0a2bca7eeef7fb5f5524890a7 +configobj==5.0.6 \ + --hash=sha256:a2f5650770e1c87fb335af19a9b7eb73fc05ccf22144eb68db7d00cd2bcb0902 +cryptography==1.2.3 \ + --hash=sha256:031938f73a5c5eb3e809e18ff7caeb6865351871417be6050cb8c86a9a202b9a \ + --hash=sha256:a179a38d50f8d68b491d7a313db78f8cabe290842cecddddc7b34d408e59db0a \ + --hash=sha256:906c88b2aadcf99cfabb24098263d1bf65ab0c8688acde10dae1f09d865920f1 \ + --hash=sha256:6e706c5c6088770b1d1b634e959e21963e315b0255f5f4777125ad3d54082977 \ + --hash=sha256:f5ebf8e31c48f8707921dca0e994de77813a9c9b9bf03c119c5ddf97bdcffe73 \ + --hash=sha256:c7b89e42288cc7fbee3812e99ef5c744f22452e11d6822f6807afc6d6b3be83e \ + --hash=sha256:8408d29865947109d8b68f1837a7cde1aa4dc86e0f79ca3ba58c0c44e443d6a5 \ + --hash=sha256:c7e76cf3c3d925dd31fa238cfb806cffba718c0f08707d77a538768477969956 \ + --hash=sha256:7d8de35380f31702758b7753bb5c40723832c73006dedb2f9099bf61a37f7287 \ + --hash=sha256:5edbee71fae5469ee83fe0a37866b9398c8ce3a46325c24fcedfbf097bb48a19 \ + --hash=sha256:594edafe4801c13bdc1cc305e7704a90c19617e95936f6ab457ee4ffe000ba50 \ + --hash=sha256:b7fdb16a0a7f481be42da744bfe1ea2163025de21f90f2c688a316f3c354da9c \ + --hash=sha256:207b8bf0fe0907336df38b733b487521cf9e138189aba9234ad54fe545dd0db8 \ + --hash=sha256:509a2f05386270cf783993c90d49ffefb3dd62aee45bf1ea8ce3d2cde7271c21 \ + --hash=sha256:ac69b65dd1af0179ede40c9f15788c88f73e628ea6c0519de3838e279bb388c6 \ + --hash=sha256:8df6fad6c6ae12fd7004ea29357f0a2b4d3774eaeca7656530d08d2d90cd41aa \ + --hash=sha256:0b8b96dd81cc1533a04f30382c0fe21c1972e189f794d0c4261a18cec08fd9b5 \ + --hash=sha256:cae8fca1883f23c50ea78d89de6fe4fefdb4cea83177760f47177559414ded93 \ + --hash=sha256:1a471ca576a9cdce1b1cd9f3a22b1d09ee44d46862037557de17919c0db44425 \ + --hash=sha256:8ec4e8e3d453b3a1b63b5f57737a434dcf1ee4a2f26f6ff7c5a37c3f679104d2 \ + --hash=sha256:8eb11c77dd8e73f48df6b2f7a7e16173fe0fe8fdfe266232832e88477e08454e +enum34==1.1.2 \ + --hash=sha256:2475d7fcddf5951e92ff546972758802de5260bf409319a9f1934e6bbc8b1dc7 \ + --hash=sha256:35907defb0f992b75ab7788f65fedc1cf20ffa22688e0e6f6f12afc06b3ea501 +funcsigs==0.4 \ + --hash=sha256:ff5ad9e2f8d9e5d1e8bbfbcf47722ab527cf0d51caeeed9da6d0f40799383fde \ + --hash=sha256:d83ce6df0b0ea6618700fe1db353526391a8a3ada1b7aba52fed7a61da772033 +idna==2.0 \ + --hash=sha256:9b2fc50bd3c4ba306b9651b69411ef22026d4d8335b93afc2214cef1246ce707 \ + --hash=sha256:16199aad938b290f5be1057c0e1efc6546229391c23cea61ca940c115f7d3d3b +ipaddress==1.0.16 \ + --hash=sha256:935712800ce4760701d89ad677666cd52691fd2f6f0b340c8b4239a3c17988a5 \ + --hash=sha256:5a3182b322a706525c46282ca6f064d27a02cffbd449f9f47416f1dc96aa71b0 +linecache2==1.0.0 \ + --hash=sha256:e78be9c0a0dfcbac712fe04fbf92b96cddae80b1b842f24248214c8496f006ef \ + --hash=sha256:4b26ff4e7110db76eeb6f5a7b64a82623839d595c2038eeda662f2a2db78e97c +ndg-httpsclient==0.4.0 \ + --hash=sha256:e8c155fdebd9c4bcb0810b4ed01ae1987554b1ee034dd7532d7b8fdae38a6274 +ordereddict==1.1 \ + --hash=sha256:1c35b4ac206cef2d24816c89f89cf289dd3d38cf7c449bb3fab7bf6d43f01b1f +parsedatetime==2.1 \ + --hash=sha256:ce9d422165cf6e963905cd5f74f274ebf7cc98c941916169178ef93f0e557838 \ + --hash=sha256:17c578775520c99131634e09cfca5a05ea9e1bd2a05cd06967ebece10df7af2d +pbr==1.8.1 \ + --hash=sha256:46c8db75ae75a056bd1cc07fa21734fe2e603d11a07833ecc1eeb74c35c72e0c \ + --hash=sha256:e2127626a91e6c885db89668976db31020f0af2da728924b56480fc7ccf09649 +psutil==3.3.0 \ + --hash=sha256:584f0b29fcc5d523b433cb8918b2fc74d67e30ee0b44a95baf031528f424619f \ + --hash=sha256:28ca0b6e9d99aa8dc286e8747a4471362b69812a25291de29b6a8d70a1545a0d \ + --hash=sha256:167ad5fff52a672c4ddc1c1a0b25146d6813ebb08a9aab0a3ac45f8a5b669c3b \ + --hash=sha256:e6dea6173a988727bb223d3497349ad5cdef5c0b282eff2d83e5f9065c53f85f \ + --hash=sha256:2af5e0a4aad66049955d0734aa4e3dc8caa17a9eaf8b4c1a27a5f1ee6e40f6fc \ + --hash=sha256:d9884dc0dc2e55e2448e495778dc9899c1c8bf37aeb2f434c1bea74af93c2683 \ + --hash=sha256:e27c2fe6dfcc8738be3d2c5a022f785eb72971057e1a9e1e34fba73bce8a71a6 \ + --hash=sha256:65afd6fecc8f3aed09ee4be63583bc8eb472f06ceaa4fe24c4d1d5a1a3c0e13f \ + --hash=sha256:ba1c558fbfcdf94515c2394b1155c1dc56e2bc2a9c17d30349827c9ed8a67e46 \ + --hash=sha256:ba95ea0022dcb64d36f0c1335c0605fae35bdf3e0fea8d92f5d0f6456a35e55b \ + --hash=sha256:421b6591d16b509aaa8d8c15821d66bb94cb4a8dc4385cad5c51b85d4a096d85 \ + --hash=sha256:326b305cbdb6f94dafbfe2c26b11da88b0ab07b8a07f8188ab9d75ff0c6e841a \ + --hash=sha256:9aede5b2b6fe46b3748ea8e5214443890d1634027bef3d33b7dad16556830278 \ + --hash=sha256:73bed1db894d1aa9c3c7e611d302cdeab7ae8a0dc0eeaf76727878db1ac5cd87 \ + --hash=sha256:935b5dd6d558af512f42501a7c08f41d7aff139af1bb3959daa3abb859234d6c \ + --hash=sha256:4ca0111cf157dcc0f2f69a323c5b5478718d68d45fc9435d84be0ec0f186215b \ + --hash=sha256:b6f13c95398a3fcf0226c4dcfa448560ba5865259cd96ec2810658651e932189 \ + --hash=sha256:ee6be30d1635bbdea4c4325d507dc8a0dbbde7e1c198bd62ddb9f43198b9e214 \ + --hash=sha256:dfa786858c268d7fbbe1b6175e001ec02738d7cfae0a7ce77bf9b651af676729 \ + --hash=sha256:aa77f9de72af9c16cc288cd4a24cf58824388f57d7a81e400c4616457629870e \ + --hash=sha256:f500093357d04da8140d87932cac2e54ef592a54ca8a743abb2850f60c2c22eb +pyasn1==0.1.9 \ + --hash=sha256:61f9d99e3cef65feb1bfe3a2eef7a93eb93819d345bf54bcd42f4e63d5204dae \ + --hash=sha256:1802a6dd32045e472a419db1441aecab469d33e0d2749e192abdec52101724af \ + --hash=sha256:35025cd9422c96504912f04e2f15fe79390a8597b430c2ca5d0534cf9309ffa0 \ + --hash=sha256:2f96ed5a0c329ca16230b326ca12b7461ec8f65e0be3e4f997516f36bf82a345 \ + --hash=sha256:28fee44217991cfad9e6a0b9f7e3f26041e21ebc96629e94e585ccd05d49fa65 \ + --hash=sha256:326e7a854a17fab07691204747695f8f692d674588a355c441fb14f660bf4e68 \ + --hash=sha256:cda5a90485709ca6795c86056c3e5fe7266028b05e53f1d527fdf93a6365a6b8 \ + --hash=sha256:0cb2a14742b543fdd68f931a14ce3829186ed2b1b2267a06787388c96b2dd9be \ + --hash=sha256:5191ff6b9126d2c039dd87f8ff025bed274baf07fa78afa46f556b1ad7265d6e \ + --hash=sha256:8323e03637b2d072cc7041300bac6ec448c3c28950ab40376036788e9a1af629 \ + --hash=sha256:853cacd96d1f701ddd67aa03ecc05f51890135b7262e922710112f12a2ed2a7f +pyOpenSSL==0.15.1 \ + --hash=sha256:88e45e6bb25dfed272a1ef2e728461d44b634c2cd689e989b6e56a349c5a3ae5 \ + --hash=sha256:f0a26070d6db0881de8bcc7846934b7c3c930d8f9c79d45883ee48984bc0d672 +pyRFC3339==1.0 \ + --hash=sha256:eea31835c56e2096af4363a5745a784878a61d043e247d3a6d6a0a32a9741f56 \ + --hash=sha256:8dfbc6c458b8daba1c0f3620a8c78008b323a268b27b7359e92a4ae41325f535 +python-augeas==0.5.0 \ + --hash=sha256:67d59d66cdba8d624e0389b87b2a83a176f21f16a87553b50f5703b23f29bac2 +python2-pythondialog==3.3.0 \ + --hash=sha256:04e93f24995c43dd90f338d5d865ca72ce3fb5a5358d4daa4965571db35fc3ec \ + --hash=sha256:3e6f593fead98f8a526bc3e306933533236e33729f552f52896ea504f55313fa +pytz==2015.7 \ + --hash=sha256:3abe6a6d3fc2fbbe4c60144211f45da2edbe3182a6f6511af6bbba0598b1f992 \ + --hash=sha256:939ef9c1e1224d980405689a97ffcf7828c56d1517b31d73464356c1f2b7769e \ + --hash=sha256:ead4aefa7007249e05e51b01095719d5a8dd95760089f5730aac5698b1932918 \ + --hash=sha256:3cca0df08bd0ed98432390494ce3ded003f5e661aa460be7a734bffe35983605 \ + --hash=sha256:3ede470d3d17ba3c07638dfa0d10452bc1b6e5ad326127a65ba77e6aaeb11bec \ + --hash=sha256:68c47964f7186eec306b13629627722b9079cd4447ed9e5ecaecd4eac84ca734 \ + --hash=sha256:dd5d3991950aae40a6c81de1578942e73d629808cefc51d12cd157980e6cfc18 \ + --hash=sha256:a77c52062c07eb7c7b30545dbc73e32995b7e117eea750317b5cb5c7a4618f14 \ + --hash=sha256:81af9aec4bc960a9a0127c488f18772dae4634689233f06f65443e7b11ebeb51 \ + --hash=sha256:e079b1dadc5c06246cc1bb6fe1b23a50b1d1173f2edd5104efd40bb73a28f406 \ + --hash=sha256:fbd26746772c24cb93c8b97cbdad5cb9e46c86bbdb1b9d8a743ee00e2fb1fc5d \ + --hash=sha256:99266ef30a37e43932deec2b7ca73e83c8dbc3b9ff703ec73eca6b1dae6befea \ + --hash=sha256:8b6ce1c993909783bc96e0b4f34ea223bff7a4df2c90bdb9c4e0f1ac928689e3 +requests==2.9.1 \ + --hash=sha256:113fbba5531a9e34945b7d36b33a084e8ba5d0664b703c81a7c572d91919a5b8 \ + --hash=sha256:c577815dd00f1394203fc44eb979724b098f88264a9ef898ee45b8e5e9cf587f +six==1.10.0 \ + --hash=sha256:0ff78c403d9bccf5a425a6d31a12aa6b47f1c21ca4dc2573a7e2f32a97335eb1 \ + --hash=sha256:105f8d68616f8248e24bf0e9372ef04d3cc10104f1980f54d57b2ce73a5ad56a +traceback2==1.4.0 \ + --hash=sha256:8253cebec4b19094d67cc5ed5af99bf1dba1285292226e98a31929f87a5d6b23 \ + --hash=sha256:05acc67a09980c2ecfedd3423f7ae0104839eccb55fc645773e1caa0951c3030 +unittest2==1.1.0 \ + --hash=sha256:13f77d0875db6d9b435e1d4f41e74ad4cc2eb6e1d5c824996092b3430f088bb8 \ + --hash=sha256:22882a0e418c284e1f718a822b3b022944d53d2d908e1690b319a9d3eb2c0579 +zope.component==4.2.2 \ + --hash=sha256:282c112b55dd8e3c869a3571f86767c150ab1284a9ace2bdec226c592acaf81a +zope.event==4.1.0 \ + --hash=sha256:dc7a59a2fd91730d3793131a5d261b29e93ec4e2a97f1bc487ce8defee2fe786 +zope.interface==4.1.3 \ + --hash=sha256:f07b631f7a601cd8cbd3332d54f43142c7088a83299f859356f08d1d4d4259b3 \ + --hash=sha256:de5cca083b9439d8002fb76bbe6b4998c5a5a721fab25b84298967f002df4c94 \ + --hash=sha256:6788416f7ea7f5b8a97be94825377aa25e8bdc73463e07baaf9858b29e737077 \ + --hash=sha256:6f3230f7254518201e5a3708cbb2de98c848304f06e3ded8bfb39e5825cba2e1 \ + --hash=sha256:5fa575a5240f04200c3088427d0d4b7b737f6e9018818a51d8d0f927a6a2517a \ + --hash=sha256:522194ad6a545735edd75c8a83f48d65d1af064e432a7d320d64f56bafc12e99 \ + --hash=sha256:e8c7b2d40943f71c99148c97f66caa7f5134147f57423f8db5b4825099ce9a09 \ + --hash=sha256:279024f0208601c3caa907c53876e37ad88625f7eaf1cb3842dbe360b2287017 \ + --hash=sha256:2e221a9eec7ccc58889a278ea13dcfed5ef939d80b07819a9a8b3cb1c681484f \ + --hash=sha256:69118965410ec86d44dc6b9017ee3ddbd582e0c0abeef62b3a19dbf6c8ad132b \ + --hash=sha256:d04df8686ec864d0cade8cf199f7f83aecd416109a20834d568f8310ded12dea \ + --hash=sha256:e75a947e15ee97e7e71e02ea302feb2fc62d3a2bb4668bf9dfbed43a506ac7e7 \ + --hash=sha256:4e45d22fb883222a5ab9f282a116fec5ee2e8d1a568ccff6a2d75bbd0eb6bcfc \ + --hash=sha256:bce9339bb3c7a55e0803b63d21c5839e8e479bc85c4adf42ae415b72f94facb2 \ + --hash=sha256:928138365245a0e8869a5999fbcc2a45475a0a6ed52a494d60dbdc540335fedd \ + --hash=sha256:0d841ba1bb840eea0e6489dc5ecafa6125554971f53b5acb87764441e61bceba \ + --hash=sha256:b09c8c1d47b3531c400e0195697f1414a63221de6ef478598a4f1460f7d9a392 +mock==1.0.1 \ + --hash=sha256:b839dd2d9c117c701430c149956918a423a9863b48b09c90e30a6013e7d2f44f \ + --hash=sha256:8f83080daa249d036cbccfb8ae5cc6ff007b88d6d937521371afabe7b19badbc + +# THE LINES BELOW ARE EDITED BY THE RELEASE SCRIPT; ADD ALL DEPENDENCIES ABOVE. + +acme==0.5.0 \ + --hash=sha256:ceb4127c13213f0006a564be82176b968c6b374d20d9fc78555d0658a252b275 \ + --hash=sha256:0605c63c656d33c883a05675f5db9cfb85d503f2771c885031800e0da7631abd +letsencrypt==0.5.0 \ + --hash=sha256:f90f883e99cdbdf8142335bdbf4f74a8af143ee4b4ec60fb49c6e47418c1114c \ + --hash=sha256:e38a2b70b82be79bc195307652244a3e012ec73d897d4dbd3f80cf698496d15a +letsencrypt-apache==0.5.0 \ + --hash=sha256:a767882164a7b09d9c12c80684a28a782135fdaf35654ef5a02c0b7b1d27ab8d \ + --hash=sha256:c20e7b9c517aa4a7d70e6bd9382da7259f00bc191b9e60d8e312e48837a00c41 + +UNLIKELY_EOF + # ------------------------------------------------------------------------- + cat << "UNLIKELY_EOF" > "$TEMP_DIR/pipstrap.py" +#!/usr/bin/env python +"""A small script that can act as a trust root for installing pip 8 + +Embed this in your project, and your VCS checkout is all you have to trust. In +a post-peep era, this lets you claw your way to a hash-checking version of pip, +with which you can install the rest of your dependencies safely. All it assumes +is Python 2.6 or better and *some* version of pip already installed. If +anything goes wrong, it will exit with a non-zero status code. + +""" +# This is here so embedded copies are MIT-compliant: +# Copyright (c) 2016 Erik Rose +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to +# deal in the Software without restriction, including without limitation the +# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +# sell copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +from __future__ import print_function +from hashlib import sha256 +from os.path import join +from pipes import quote +from shutil import rmtree +try: + from subprocess import check_output +except ImportError: + from subprocess import CalledProcessError, PIPE, Popen + + def check_output(*popenargs, **kwargs): + if 'stdout' in kwargs: + raise ValueError('stdout argument not allowed, it will be ' + 'overridden.') + process = Popen(stdout=PIPE, *popenargs, **kwargs) + output, unused_err = process.communicate() + retcode = process.poll() + if retcode: + cmd = kwargs.get("args") + if cmd is None: + cmd = popenargs[0] + raise CalledProcessError(retcode, cmd) + return output +from sys import exit, version_info +from tempfile import mkdtemp +try: + from urllib2 import build_opener, HTTPHandler, HTTPSHandler +except ImportError: + from urllib.request import build_opener, HTTPHandler, HTTPSHandler +try: + from urlparse import urlparse +except ImportError: + from urllib.parse import urlparse # 3.4 + + +__version__ = 1, 1, 1 + + +# wheel has a conditional dependency on argparse: +maybe_argparse = ( + [('https://pypi.python.org/packages/source/a/argparse/' + 'argparse-1.4.0.tar.gz', + '62b089a55be1d8949cd2bc7e0df0bddb9e028faefc8c32038cc84862aefdd6e4')] + if version_info < (2, 7, 0) else []) + + +PACKAGES = maybe_argparse + [ + # Pip has no dependencies, as it vendors everything: + ('https://pypi.python.org/packages/source/p/pip/pip-8.0.3.tar.gz', + '30f98b66f3fe1069c529a491597d34a1c224a68640c82caf2ade5f88aa1405e8'), + # This version of setuptools has only optional dependencies: + ('https://pypi.python.org/packages/source/s/setuptools/' + 'setuptools-20.2.2.tar.gz', + '24fcfc15364a9fe09a220f37d2dcedc849795e3de3e4b393ee988e66a9cbd85a'), + ('https://pypi.python.org/packages/source/w/wheel/wheel-0.29.0.tar.gz', + '1ebb8ad7e26b448e9caa4773d2357849bf80ff9e313964bcaf79cbf0201a1648') +] + + +class HashError(Exception): + def __str__(self): + url, path, actual, expected = self.args + return ('{url} did not match the expected hash {expected}. Instead, ' + 'it was {actual}. The file (left at {path}) may have been ' + 'tampered with.'.format(**locals())) + + +def hashed_download(url, temp, digest): + """Download ``url`` to ``temp``, make sure it has the SHA-256 ``digest``, + and return its path.""" + # Based on pip 1.4.1's URLOpener but with cert verification removed. Python + # >=2.7.9 verifies HTTPS certs itself, and, in any case, the cert + # authenticity has only privacy (not arbitrary code execution) + # implications, since we're checking hashes. + def opener(): + opener = build_opener(HTTPSHandler()) + # Strip out HTTPHandler to prevent MITM spoof: + for handler in opener.handlers: + if isinstance(handler, HTTPHandler): + opener.handlers.remove(handler) + return opener + + def read_chunks(response, chunk_size): + while True: + chunk = response.read(chunk_size) + if not chunk: + break + yield chunk + + response = opener().open(url) + path = join(temp, urlparse(url).path.split('/')[-1]) + actual_hash = sha256() + with open(path, 'wb') as file: + for chunk in read_chunks(response, 4096): + file.write(chunk) + actual_hash.update(chunk) + + actual_digest = actual_hash.hexdigest() + if actual_digest != digest: + raise HashError(url, path, actual_digest, digest) + return path + + +def main(): + temp = mkdtemp(prefix='pipstrap-') + try: + downloads = [hashed_download(url, temp, digest) + for url, digest in PACKAGES] + check_output('pip install --no-index --no-deps -U ' + + ' '.join(quote(d) for d in downloads), + shell=True) + except HashError as exc: + print(exc) + except Exception: + rmtree(temp) + raise + else: + rmtree(temp) + return 0 + return 1 + + +if __name__ == '__main__': + exit(main()) + +UNLIKELY_EOF + # ------------------------------------------------------------------------- + # Set PATH so pipstrap upgrades the right (v)env: + PATH="$VENV_BIN:$PATH" "$VENV_BIN/python" "$TEMP_DIR/pipstrap.py" + set +e + PIP_OUT=`"$VENV_BIN/pip" install --no-cache-dir --require-hashes -r "$TEMP_DIR/letsencrypt-auto-requirements.txt" 2>&1` + PIP_STATUS=$? + set -e + rm -rf "$TEMP_DIR" + if [ "$PIP_STATUS" != 0 ]; then + # Report error. (Otherwise, be quiet.) + echo "Had a problem while installing Python packages:" + echo "$PIP_OUT" + rm -rf "$VENV_PATH" + exit 1 + fi + echo "Installation succeeded." + fi + echo "Requesting root privileges to run letsencrypt..." + echo " " $SUDO "$VENV_BIN/letsencrypt" "$@" + $SUDO "$VENV_BIN/letsencrypt" "$@" +else + # Phase 1: Upgrade letsencrypt-auto if neceesary, then self-invoke. + # + # Each phase checks the version of only the thing it is responsible for + # upgrading. Phase 1 checks the version of the latest release of + # letsencrypt-auto (which is always the same as that of the letsencrypt + # package). Phase 2 checks the version of the locally installed letsencrypt. + + if [ ! -f "$VENV_BIN/letsencrypt" ]; then + # If it looks like we've never bootstrapped before, bootstrap: + Bootstrap + fi + if [ "$OS_PACKAGES_ONLY" = 1 ]; then + echo "OS packages installed." + exit 0 + fi + + if [ "$NO_SELF_UPGRADE" != 1 ]; then + echo "Checking for new version..." + TEMP_DIR=$(TempDir) + # --------------------------------------------------------------------------- + cat << "UNLIKELY_EOF" > "$TEMP_DIR/fetch.py" +"""Do downloading and JSON parsing without additional dependencies. :: + + # Print latest released version of LE to stdout: + python fetch.py --latest-version + + # Download letsencrypt-auto script from git tag v1.2.3 into the folder I'm + # in, and make sure its signature verifies: + python fetch.py --le-auto-script v1.2.3 + +On failure, return non-zero. + +""" +from distutils.version import LooseVersion +from json import loads +from os import devnull, environ +from os.path import dirname, join +import re +from subprocess import check_call, CalledProcessError +from sys import argv, exit +from urllib2 import build_opener, HTTPHandler, HTTPSHandler, HTTPError + +PUBLIC_KEY = environ.get('LE_AUTO_PUBLIC_KEY', """-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA6MR8W/galdxnpGqBsYbq +OzQb2eyW15YFjDDEMI0ZOzt8f504obNs920lDnpPD2/KqgsfjOgw2K7xWDJIj/18 +xUvWPk3LDkrnokNiRkA3KOx3W6fHycKL+zID7zy+xZYBuh2fLyQtWV1VGQ45iNRp +9+Zo7rH86cdfgkdnWTlNSHyTLW9NbXvyv/E12bppPcEvgCTAQXgnDVJ0/sqmeiij +n9tTFh03aM+R2V/21h8aTraAS24qiPCz6gkmYGC8yr6mglcnNoYbsLNYZ69zF1XH +cXPduCPdPdfLlzVlKK1/U7hkA28eG3BIAMh6uJYBRJTpiGgaGdPd7YekUB8S6cy+ +CQIDAQAB +-----END PUBLIC KEY----- +""") + +class ExpectedError(Exception): + """A novice-readable exception that also carries the original exception for + debugging""" + + +class HttpsGetter(object): + def __init__(self): + """Build an HTTPS opener.""" + # Based on pip 1.4.1's URLOpener + # This verifies certs on only Python >=2.7.9. + self._opener = build_opener(HTTPSHandler()) + # Strip out HTTPHandler to prevent MITM spoof: + for handler in self._opener.handlers: + if isinstance(handler, HTTPHandler): + self._opener.handlers.remove(handler) + + def get(self, url): + """Return the document contents pointed to by an HTTPS URL. + + If something goes wrong (404, timeout, etc.), raise ExpectedError. + + """ + try: + return self._opener.open(url).read() + except (HTTPError, IOError) as exc: + raise ExpectedError("Couldn't download %s." % url, exc) + + +def write(contents, dir, filename): + """Write something to a file in a certain directory.""" + with open(join(dir, filename), 'w') as file: + file.write(contents) + + +def latest_stable_version(get): + """Return the latest stable release of letsencrypt.""" + metadata = loads(get( + environ.get('LE_AUTO_JSON_URL', + 'https://pypi.python.org/pypi/letsencrypt/json'))) + # metadata['info']['version'] actually returns the latest of any kind of + # release release, contrary to https://wiki.python.org/moin/PyPIJSON. + # The regex is a sufficient regex for picking out prereleases for most + # packages, LE included. + return str(max(LooseVersion(r) for r + in metadata['releases'].iterkeys() + if re.match('^[0-9.]+$', r))) + + +def verified_new_le_auto(get, tag, temp_dir): + """Return the path to a verified, up-to-date letsencrypt-auto script. + + If the download's signature does not verify or something else goes wrong + with the verification process, raise ExpectedError. + + """ + le_auto_dir = environ.get( + 'LE_AUTO_DIR_TEMPLATE', + 'https://raw.githubusercontent.com/letsencrypt/letsencrypt/%s/' + 'letsencrypt-auto-source/') % tag + write(get(le_auto_dir + 'letsencrypt-auto'), temp_dir, 'letsencrypt-auto') + write(get(le_auto_dir + 'letsencrypt-auto.sig'), temp_dir, 'letsencrypt-auto.sig') + write(PUBLIC_KEY, temp_dir, 'public_key.pem') + try: + with open(devnull, 'w') as dev_null: + check_call(['openssl', 'dgst', '-sha256', '-verify', + join(temp_dir, 'public_key.pem'), + '-signature', + join(temp_dir, 'letsencrypt-auto.sig'), + join(temp_dir, 'letsencrypt-auto')], + stdout=dev_null, + stderr=dev_null) + except CalledProcessError as exc: + raise ExpectedError("Couldn't verify signature of downloaded " + "letsencrypt-auto.", exc) + + +def main(): + get = HttpsGetter().get + flag = argv[1] + try: + if flag == '--latest-version': + print latest_stable_version(get) + elif flag == '--le-auto-script': + tag = argv[2] + verified_new_le_auto(get, tag, dirname(argv[0])) + except ExpectedError as exc: + print exc.args[0], exc.args[1] + return 1 + else: + return 0 + + +if __name__ == '__main__': + exit(main()) + +UNLIKELY_EOF + # --------------------------------------------------------------------------- + DeterminePythonVersion + REMOTE_VERSION=`"$LE_PYTHON" "$TEMP_DIR/fetch.py" --latest-version` + if [ "$LE_AUTO_VERSION" != "$REMOTE_VERSION" ]; then + echo "Upgrading letsencrypt-auto $LE_AUTO_VERSION to $REMOTE_VERSION..." + + # Now we drop into Python so we don't have to install even more + # dependencies (curl, etc.), for better flow control, and for the option of + # future Windows compatibility. + "$LE_PYTHON" "$TEMP_DIR/fetch.py" --le-auto-script "v$REMOTE_VERSION" + + # Install new copy of letsencrypt-auto. + # TODO: Deal with quotes in pathnames. + echo "Replacing letsencrypt-auto..." + # Clone permissions with cp. chmod and chown don't have a --reference + # option on OS X or BSD, and stat -c on Linux is stat -f on OS X and BSD: + echo " " $SUDO cp -p "$0" "$TEMP_DIR/letsencrypt-auto.permission-clone" + $SUDO cp -p "$0" "$TEMP_DIR/letsencrypt-auto.permission-clone" + echo " " $SUDO cp "$TEMP_DIR/letsencrypt-auto" "$TEMP_DIR/letsencrypt-auto.permission-clone" + $SUDO cp "$TEMP_DIR/letsencrypt-auto" "$TEMP_DIR/letsencrypt-auto.permission-clone" + # Using mv rather than cp leaves the old file descriptor pointing to the + # original copy so the shell can continue to read it unmolested. mv across + # filesystems is non-atomic, doing `rm dest, cp src dest, rm src`, but the + # cp is unlikely to fail (esp. under sudo) if the rm doesn't. + echo " " $SUDO mv -f "$TEMP_DIR/letsencrypt-auto.permission-clone" "$0" + $SUDO mv -f "$TEMP_DIR/letsencrypt-auto.permission-clone" "$0" + # TODO: Clean up temp dir safely, even if it has quotes in its path. + rm -rf "$TEMP_DIR" + fi # A newer version is available. + fi # Self-upgrading is allowed. + + "$0" --le-auto-phase2 "$@" +fi diff --git a/letsencrypt-auto-source/certbot-auto.asc b/letsencrypt-auto-source/certbot-auto.asc new file mode 100644 index 000000000..593e644ec --- /dev/null +++ b/letsencrypt-auto-source/certbot-auto.asc @@ -0,0 +1,11 @@ +-----BEGIN PGP SIGNATURE----- +Version: GnuPG v1 + +iQEcBAABAgAGBQJXNLdqAAoJEE0XyZXNl3Xyk3cH/1OxJ16VbX9PK4U60mVRPseA +wzaCFOCVVB3Ub9m6PMzOw6kV9KLXuiMgh1qj9j4AH5UdFwynv6WNe+pxO3deYO6O +DBhlW0ibDoIGLb2nvHDmBXfX+CE8ixvo2pp7ao/StV5MTHSoDHfMZbT7ql1xpx7U +XOaPfl8MlmIlii6QFrBNZpUjE03NE8xUpocZq53eXxZiCRHLbO9z4j1e6NkaTotj +4Q1o9ERdAc5S7czF9TFwPgQWXoVxiMjhUUpNjfVFFjFB6r76AdVzz5PG6icZ6icM +C3kD1N4gm0sggNkWwDOfNAgTanVa/pmIv/wi7tkm/jpCfkErttnq9AjEhichaSY= +=SJou +-----END PGP SIGNATURE----- From a7d0b1a7d38111eeea96bf3aaa0eedee46ecc234 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Thu, 12 May 2016 17:49:44 -0700 Subject: [PATCH 095/396] Address review comments --- certbot/cli.py | 2 +- certbot/crypto_util.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/certbot/cli.py b/certbot/cli.py index 796925c1d..430384a5a 100644 --- a/certbot/cli.py +++ b/certbot/cli.py @@ -349,7 +349,7 @@ class HelpfulArgumentParser(object): def set_test_server(self, parsed_args): - "We have --staging/--dry-run; perform sanity check and set config.server" + """We have --staging/--dry-run; perform sanity check and set config.server""" if parsed_args.server not in (flag_default("server"), constants.STAGING_URI): conflicts = ["--staging"] if parsed_args.staging else [] diff --git a/certbot/crypto_util.py b/certbot/crypto_util.py index ceec6db71..07e7f9fd2 100644 --- a/certbot/crypto_util.py +++ b/certbot/crypto_util.py @@ -178,11 +178,11 @@ def import_csr_file(csrfile, contents): :param str csrfile: CSR filename :param str contents: contens of the CSR file - :rtype: tuple - :returns: (le_util.CSR object representing the CSR, - OpenSSL FILETYPE_ representing DER or PEM, + `OpenSSL.crypto.FILETYPE_PEM` or `OpenSSL.crypto.FILETYPE_ASN1`, list of domains requested in the CSR) + + :rtype: tuple """ try: csr = le_util.CSR(file=csrfile, data=contents, form="der") From 5b058fd18f2565624642862d5c8a51ec84f793e8 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Thu, 12 May 2016 19:06:09 -0700 Subject: [PATCH 096/396] Import third party plugin list from the wiki And clean up the confusing section about third party plugins --- docs/using.rst | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/docs/using.rst b/docs/using.rst index e7e0e9474..469e302ee 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -82,6 +82,9 @@ manual_ Y N Helps you obtain a cert by giving you instructions to perf nginx_ Y Y Very experimental and not included in letsencrypt-auto_. =========== ==== ==== =============================================================== +Third-party plugins +------------------- + There are also a number of third-party plugins for the client, provided by other developers: =========== ==== ==== =============================================================== @@ -91,15 +94,25 @@ plesk_ Y Y Integration with the Plesk web hosting tool haproxy_ Y Y Integration with the HAProxy load balancer s3front_ Y Y Integration with Amazon CloudFront distribution of S3 buckets gandi_ Y Y Integration with Gandi's hosting products and API +varnish_ Y N Obtain certs via a Varnish server +external_ Y N A plugin for convenient scripting (See also ticket 2782_) +icecast_ N Y Deploy certs to Icecast 2 streaming media servers +pritunl_ N Y Install certs in pritunl distributed OpenVPN servers +proxmox_ N Y Install certs in Proxmox Virtualization servers + =========== ==== ==== =============================================================== .. _plesk: https://github.com/plesk/letsencrypt-plesk .. _haproxy: https://code.greenhost.net/open/letsencrypt-haproxy .. _s3front: https://github.com/dlapiduz/letsencrypt-s3front .. _gandi: https://github.com/Gandi/letsencrypt-gandi +.. _icecast: https://github.com/e00E/lets-encrypt-icecast +.. _varnish: http://git.sesse.net/?p=letsencrypt-varnish-plugin +.. _2782: https://github.com/certbot/certbot/issues/2782 +.. _pritunl: https://github.com/kharkevich/letsencrypt-pritunl +.. _proxmox: https://github.com/kharkevich/letsencrypt-proxmox -Future plugins for IMAP servers, SMTP servers, IRC servers, etc, are likely to -be installers but not authenticators. +If you're interested, you can also :ref:`write your own plugin `. Apache ------ @@ -190,12 +203,6 @@ 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 `. Renewal ======= From b905cb448134065a502cd25958c1a15392060962 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Thu, 12 May 2016 19:09:59 -0700 Subject: [PATCH 097/396] Missing link --- docs/using.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/using.rst b/docs/using.rst index 469e302ee..8383f9690 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -111,6 +111,7 @@ proxmox_ N Y Install certs in Proxmox Virtualization servers .. _2782: https://github.com/certbot/certbot/issues/2782 .. _pritunl: https://github.com/kharkevich/letsencrypt-pritunl .. _proxmox: https://github.com/kharkevich/letsencrypt-proxmox +.. _external: https://github.com/marcan/letsencrypt-external If you're interested, you can also :ref:`write your own plugin `. From 2dc983db4943bd87620a88c6c9e34bcf04f56e07 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Thu, 12 May 2016 19:17:19 -0700 Subject: [PATCH 098/396] STLSE is a prototype Postfix plugin - it partially uses IInstaller - it will also support Exim in the future --- docs/using.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/using.rst b/docs/using.rst index 8383f9690..47746c110 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -99,7 +99,7 @@ external_ Y N A plugin for convenient scripting (See also ticket 2782_) icecast_ N Y Deploy certs to Icecast 2 streaming media servers pritunl_ N Y Install certs in pritunl distributed OpenVPN servers proxmox_ N Y Install certs in Proxmox Virtualization servers - +postfix_ N Y STARTTLS Everywhere is becoming a Certbot Postfix/Exim plugin =========== ==== ==== =============================================================== .. _plesk: https://github.com/plesk/letsencrypt-plesk @@ -112,6 +112,7 @@ proxmox_ N Y Install certs in Proxmox Virtualization servers .. _pritunl: https://github.com/kharkevich/letsencrypt-pritunl .. _proxmox: https://github.com/kharkevich/letsencrypt-proxmox .. _external: https://github.com/marcan/letsencrypt-external +.. _postfix: https://github.com/EFForg/starttls-everywhere If you're interested, you can also :ref:`write your own plugin `. From 3c279c4fadfc4c08a3119f39317865a1ef3dcff1 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Fri, 13 May 2016 12:22:19 -0700 Subject: [PATCH 099/396] Create a man page for ourselves! --- docs/cli-help.txt | 340 +++++++++++++++++++++++++++++++++++++++++++ docs/man/certbot.rst | 2 +- 2 files changed, 341 insertions(+), 1 deletion(-) create mode 100644 docs/cli-help.txt diff --git a/docs/cli-help.txt b/docs/cli-help.txt new file mode 100644 index 000000000..cb4bace58 --- /dev/null +++ b/docs/cli-help.txt @@ -0,0 +1,340 @@ +usage: + certbot [SUBCOMMAND] [options] [-d domain] [-d domain] ... + +Certbot can obtain and install HTTPS/TLS/SSL certificates. By default, +it will attempt to use a webserver both for obtaining and installing the +cert. Major SUBCOMMANDS are: + + (default) run Obtain & install a cert in your current webserver + certonly Obtain cert, but do not install it (aka "auth") + install Install a previously obtained cert in a server + renew Renew previously obtained certs that are near expiry + revoke Revoke a previously obtained certificate + rollback Rollback server configuration changes made during install + config_changes Show changes made to server config during installation + plugins Display information about installed plugins + +optional arguments: + -h, --help show this help message and exit + -c CONFIG_FILE, --config CONFIG_FILE + config file path (default: None) + -v, --verbose This flag can be used multiple times to incrementally + increase the verbosity of output, e.g. -vvv. (default: + -3) + -t, --text Use the text output instead of the curses UI. + (default: False) + -n, --non-interactive, --noninteractive + Run without ever asking for user input. This may + require additional command line flags; the client will + try to explain which ones are required if it finds one + missing (default: False) + --dry-run Perform a test run of the client, obtaining test + (invalid) certs but not saving them to disk. This can + currently only be used with the 'certonly' and 'renew' + subcommands. Note: Although --dry-run tries to avoid + making any persistent changes on a system, it is not + completely side-effect free: if used with webserver + authenticator plugins like apache and nginx, it makes + and then reverts temporary config changes in order to + obtain test certs, and reloads webservers to deploy + and then roll back those changes. It also calls --pre- + hook and --post-hook commands if they are defined + because they may be necessary to accurately simulate + renewal. --renew-hook commands are not called. + (default: False) + --register-unsafely-without-email + 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 or revocation of your certificates. Updates + to the Subscriber Agreement will still affect you, and + will be effective 14 days after posting an update to + the web site. (default: False) + -m EMAIL, --email EMAIL + Email used for registration and recovery contact. + (default: None) + -d DOMAIN, --domains DOMAIN, --domain DOMAIN + Domain names to apply. For multiple domains you can + use multiple -d flags or enter a comma separated list + of domains as a parameter. (default: []) + --user-agent USER_AGENT + Set a custom user agent string for the client. User + agent strings allow the CA to collect high level + statistics about success rates by OS and plugin. If + you wish to hide your server OS version from the Let's + Encrypt server, set this to "". (default: None) + +automation: + Arguments for automating execution & other tweaks + + --keep-until-expiring, --keep, --reinstall + If the requested cert matches an existing cert, always + keep the existing one until it is due for renewal (for + the 'run' subcommand this means reinstall the existing + cert) (default: False) + --expand If an existing cert covers some subset of the + requested names, always expand and replace it with the + additional names. (default: False) + --version show program's version number and exit + --force-renewal, --renew-by-default + If a certificate already exists for the requested + domains, renew it now, regardless of whether it is + near expiry. (Often --keep-until-expiring is more + appropriate). Also implies --expand. (default: False) + --allow-subset-of-names + When performing domain validation, do not consider it + a failure if authorizations can not be obtained for a + strict subset of the requested domains. This may be + useful for allowing renewals for multiple domains to + succeed even if some domains no longer point at this + system. This option cannot be used with --csr. + (default: False) + --agree-tos Agree to the ACME Subscriber Agreement (default: + False) + --account ACCOUNT_ID Account ID to use (default: None) + --duplicate Allow making a certificate lineage that duplicates an + existing one (both can be renewed in parallel) + (default: False) + --os-packages-only (letsencrypt-auto only) install OS package + dependencies and then stop (default: False) + --no-self-upgrade (letsencrypt-auto only) prevent the letsencrypt-auto + script from upgrading itself to newer released + versions (default: False) + -q, --quiet Silence all output except errors. Useful for + automation via cron. Implies --non-interactive. + (default: False) + +testing: + The following flags are meant for testing purposes only! Do NOT change + them, unless you really know what you're doing! + + --debug Show tracebacks in case of errors, and allow + letsencrypt-auto execution on experimental platforms + (default: False) + --no-verify-ssl Disable SSL certificate verification. (default: False) + --tls-sni-01-port TLS_SNI_01_PORT + Port number to perform tls-sni-01 challenge. Boulder + in testing mode defaults to 5001. (default: 443) + --http-01-port HTTP01_PORT + Port used in the SimpleHttp challenge. (default: 80) + --break-my-certs Be willing to replace or renew valid certs with + invalid (testing/staging) certs (default: False) + --test-cert, --staging + Use the staging server to obtain test (invalid) certs; + equivalent to --server https://acme- + staging.api.letsencrypt.org/directory (default: False) + +security: + Security parameters & server settings + + --rsa-key-size N Size of the RSA key. (default: 2048) + --redirect Automatically redirect all HTTP traffic to HTTPS for + the newly authenticated vhost. (default: None) + --no-redirect Do not automatically redirect all HTTP traffic to + HTTPS for the newly authenticated vhost. (default: + None) + --hsts Add the Strict-Transport-Security header to every HTTP + response. Forcing browser to use always use SSL for + the domain. Defends against SSL Stripping. (default: + False) + --no-hsts Do not automatically add the Strict-Transport-Security + header to every HTTP response. (default: False) + --uir Add the "Content-Security-Policy: upgrade-insecure- + requests" header to every HTTP response. Forcing the + browser to use https:// for every http:// resource. + (default: None) + --no-uir Do not automatically set the "Content-Security-Policy: + upgrade-insecure-requests" header to every HTTP + response. (default: None) + --strict-permissions Require that all configuration files are owned by the + current user; only needed if your config is somewhere + unsafe like /tmp/ (default: False) + +renew: + The 'renew' subcommand will attempt to renew all certificates (or more + precisely, certificate lineages) you have previously obtained if they are + close to expiry, and print a summary of the results. By default, 'renew' + will reuse the options used to create obtain or most recently successfully + renew each certificate lineage. You can try it with `--dry-run` first. For + more fine-grained control, you can renew individual lineages with the + `certonly` subcommand. Hooks are available to run commands before and + after renewal; see https://certbot.eff.org/docs/using.html#renewal for + more information on these. + + --pre-hook PRE_HOOK Command to be run in a shell before obtaining any + certificates. Intended primarily for renewal, where it + can be used to temporarily shut down a webserver that + might conflict with the standalone plugin. This will + only be called if a certificate is actually to be + obtained/renewed. (default: None) + --post-hook POST_HOOK + Command to be run in a shell after attempting to + obtain/renew certificates. Can be used to deploy + renewed certificates, or to restart any servers that + were stopped by --pre-hook. (default: None) + --renew-hook RENEW_HOOK + Command to be run in a shell once for each + successfully renewed certificate.For this command, the + shell variable $RENEWED_LINEAGE will point to + theconfig live subdirectory containing the new certs + and keys; the shell variable $RENEWED_DOMAINS will + contain a space-delimited list of renewed cert domains + (default: None) + +certonly: + Options for modifying how a cert is obtained + + --csr CSR Path to a Certificate Signing Request (CSR) in DER + format; note that the .csr file *must* contain a + Subject Alternative Name field for each domain you + want certified. Currently --csr only works with the + 'certonly' subcommand' (default: None) + +install: + Options for modifying how a cert is deployed + +revoke: + Options for revocation of certs + +rollback: + Options for reverting config changes + + --checkpoints N Revert configuration N number of checkpoints. + (default: 1) + +plugins: + Plugin options + + --init Initialize plugins. (default: False) + --prepare Initialize and prepare plugins. (default: False) + --authenticators Limit to authenticator plugins only. (default: None) + --installers Limit to installer plugins only. (default: None) + +config_changes: + Options for showing a history of config changes + + --num NUM How many past revisions you want to be displayed + (default: None) + +paths: + Arguments changing execution paths & servers + + --cert-path CERT_PATH + Path to where cert is saved (with auth --csr), + installed from or revoked. (default: None) + --key-path KEY_PATH Path to private key for cert installation or + revocation (if account key is missing) (default: None) + --fullchain-path FULLCHAIN_PATH + Accompanying path to a full certificate chain (cert + plus chain). (default: None) + --chain-path CHAIN_PATH + Accompanying path to a certificate chain. (default: + None) + --config-dir CONFIG_DIR + Configuration directory. (default: /etc/letsencrypt) + --work-dir WORK_DIR Working directory. (default: /var/lib/letsencrypt) + --logs-dir LOGS_DIR Logs directory. (default: /var/log/letsencrypt) + --server SERVER ACME Directory Resource URI. (default: + https://acme-v01.api.letsencrypt.org/directory) + +plugins: + Certbot client supports an extensible plugins architecture. See 'certbot + plugins' for a list of all installed plugins and their names. You can + force a particular plugin by setting options provided below. Running + --help will list flags specific to that plugin. + + -a AUTHENTICATOR, --authenticator AUTHENTICATOR + Authenticator plugin name. (default: None) + -i INSTALLER, --installer INSTALLER + Installer plugin name (also used to find domains). + (default: None) + --configurator CONFIGURATOR + Name of the plugin that is both an authenticator and + an installer. Should not be used together with + --authenticator or --installer. (default: None) + --apache Obtain and install certs using Apache (default: False) + --nginx Obtain and install certs using Nginx (default: False) + --standalone Obtain certs using a "standalone" webserver. (default: + False) + --manual Provide laborious manual instructions for obtaining a + cert (default: False) + --webroot Obtain certs by placing files in a webroot directory. + (default: False) + +nginx: + Nginx Web Server - currently doesn't work + + --nginx-server-root NGINX_SERVER_ROOT + Nginx server root directory. (default: /etc/nginx) + --nginx-ctl NGINX_CTL + Path to the 'nginx' binary, used for 'configtest' and + retrieving nginx version number. (default: nginx) + +standalone: + Automatically use a temporary webserver + + --standalone-supported-challenges STANDALONE_SUPPORTED_CHALLENGES + Supported challenges. Preferred in the order they are + listed. (default: tls-sni-01,http-01) + +manual: + Manually configure an HTTP server + + --manual-test-mode Test mode. Executes the manual command in subprocess. + (default: False) + --manual-public-ip-logging-ok + Automatically allows public IP logging. (default: + False) + +webroot: + Place files in webroot directory + + --webroot-path WEBROOT_PATH, -w WEBROOT_PATH + public_html / webroot path. This can be specified + multiple times to handle different domains; each + domain will have the webroot path that preceded it. + For instance: `-w /var/www/example -d example.com -d + www.example.com -w /var/www/thing -d thing.net -d + m.thing.net` (default: []) + --webroot-map WEBROOT_MAP + JSON dictionary mapping domains to webroot paths; this + implies -d for each entry. You may need to escape this + from your shell. E.g.: --webroot-map + '{"eg1.is,m.eg1.is":"/www/eg1/", "eg2.is":"/www/eg2"}' + This option is merged with, but takes precedence over, + -w / -d entries. At present, if you put webroot-map in + a config file, it needs to be on a single line, like: + webroot-map = {"example.com":"/var/www"}. (default: + {}) + +apache: + Apache Web Server - Alpha + + --apache-enmod APACHE_ENMOD + Path to the Apache 'a2enmod' binary. (default: + a2enmod) + --apache-dismod APACHE_DISMOD + Path to the Apache 'a2dismod' binary. (default: + a2dismod) + --apache-le-vhost-ext APACHE_LE_VHOST_EXT + SSL vhost configuration extension. (default: -le- + ssl.conf) + --apache-server-root APACHE_SERVER_ROOT + Apache server root directory. (default: /etc/apache2) + --apache-vhost-root APACHE_VHOST_ROOT + Apache server VirtualHost configuration root (default: + /etc/apache2/sites-available) + --apache-challenge-location APACHE_CHALLENGE_LOCATION + Directory path for challenge configuration. (default: + /etc/apache2) + --apache-handle-modules APACHE_HANDLE_MODULES + Let installer handle enabling required modules for + you.(Only Ubuntu/Debian currently) (default: True) + --apache-handle-sites APACHE_HANDLE_SITES + Let installer handle enabling sites for you.(Only + Ubuntu/Debian currently) (default: True) + +null: + Null Installer diff --git a/docs/man/certbot.rst b/docs/man/certbot.rst index 7382d7811..8fb03db49 100644 --- a/docs/man/certbot.rst +++ b/docs/man/certbot.rst @@ -1 +1 @@ -.. program-output:: certbot --help all +.. literalinclude:: cli-help.txt From c55d8e4741bfcd02934c37902f9ba6edf33cb8ee Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Fri, 13 May 2016 12:22:35 -0700 Subject: [PATCH 100/396] Build the text for the man page at release --- tools/release.sh | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tools/release.sh b/tools/release.sh index 8c2d04cd4..abaad09ff 100755 --- a/tools/release.sh +++ b/tools/release.sh @@ -145,6 +145,9 @@ pip install \ kill $! cd ~- +# get a snapshot of the CLI help for the docs +certbot --help all > docs/cli-help.txt + # freeze before installing anything else, so that we know end-user KGS # make sure "twine upload" doesn't catch "kgs" if [ -d ../kgs ] ; then @@ -197,7 +200,7 @@ mv letsencrypt-auto-source/letsencrypt-auto.asc letsencrypt-auto-source/certbot- cp -p letsencrypt-auto-source/letsencrypt-auto certbot-auto cp -p letsencrypt-auto-source/letsencrypt-auto letsencrypt-auto -git add certbot-auto letsencrypt-auto letsencrypt-auto-source +git add certbot-auto letsencrypt-auto letsencrypt-auto-source docs/cli-help.txt git diff --cached git commit --gpg-sign="$RELEASE_GPG_KEY" -m "Release $version" git tag --local-user "$RELEASE_GPG_KEY" --sign --message "Release $version" "$tag" From f16104e3cb1b1de34d4683853d3b255229c9ae4d Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Fri, 13 May 2016 12:22:55 -0700 Subject: [PATCH 101/396] Lots of doc cleanups --- README.rst | 14 +++++--- certbot/cli.py | 3 +- docs/contributing.rst | 2 +- docs/intro.rst | 6 ++-- docs/using.rst | 78 +++++++++++++++---------------------------- 5 files changed, 42 insertions(+), 61 deletions(-) diff --git a/README.rst b/README.rst index 20b49083f..c71079f9a 100644 --- a/README.rst +++ b/README.rst @@ -31,14 +31,17 @@ Contributing If you'd like to contribute to this project please read `Developer Guide `_. +.. _installation: + Installation ------------ -If ``certbot`` (or ``letsencrypt``) is packaged for your Unix OS, you can install -it from there, and run it by typing ``certbot`` (or ``letsencrypt``). -Because not all operating systems have packages yet, we provide a temporary -solution via the ``certbot-auto`` wrapper script, which obtains some -dependencies from your OS and puts others in a python virtual environment:: +If ``certbot`` (or ``letsencrypt``) is packaged for your Unix OS (visit +certbot.eff.org_ to find out), you can install it +from there, and run it by typing ``certbot`` (or ``letsencrypt``). Because +not all operating systems have packages yet, we provide a temporary solution +via the ``certbot-auto`` wrapper script, which obtains some dependencies from +your OS and puts others in a python virtual environment:: user@webserver:~$ wget https://dl.eff.org/certbot-auto user@webserver:~$ chmod a+x ./certbot-auto @@ -188,3 +191,4 @@ Current Features .. _Freenode: https://webchat.freenode.net?channels=%23letsencrypt .. _OFTC: https://webchat.oftc.net?channels=%23certbot .. _client-dev: https://groups.google.com/a/letsencrypt.org/forum/#!forum/client-dev +.. _certbot.eff.org: https://certbot.eff.org/ diff --git a/certbot/cli.py b/certbot/cli.py index 90e86a751..65715ac57 100644 --- a/certbot/cli.py +++ b/certbot/cli.py @@ -745,7 +745,8 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): " certificate lineage. You can try it with `--dry-run` first. For" " more fine-grained control, you can renew individual lineages with" " the `certonly` subcommand. Hooks are available to run commands " - " before and after renewal; see XXX for more information on these.") + " before and after renewal; see" + " https://certbot.eff.org/docs/using.html#renewal for more information on these.") helpful.add( "renew", "--pre-hook", diff --git a/docs/contributing.rst b/docs/contributing.rst index 49e9e146d..b56a04c7d 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -333,7 +333,7 @@ commands: .. code-block:: shell - make -C docs clean html + make -C docs clean html man This should generate documentation in the ``docs/_build/html`` directory. diff --git a/docs/intro.rst b/docs/intro.rst index 188ff4302..2fffbec68 100644 --- a/docs/intro.rst +++ b/docs/intro.rst @@ -1,6 +1,6 @@ -============ -Introduction -============ +===================== +README / Introduction +===================== .. include:: ../README.rst .. include:: ../CHANGES.rst diff --git a/docs/using.rst b/docs/using.rst index 997134de5..6558e9e16 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -5,56 +5,28 @@ User Guide .. contents:: Table of Contents :local: -.. _installation: +Getting Certbot +=============== -Installation -============ +To get specific instructions for installing Certbot on your OS, we recommend +visiting certbot.eff.org_. If you're offline, you can find some general +instructions `in the README / Introduction `__ + +__ installation_ +.. _certbot.eff.org: https://certbot.eff.org .. _certbot-auto: -certbot-auto ----------------- +The name of the certbot command +------------------------------- -``certbot-auto`` is a wrapper which installs some dependencies -from your OS standard package repositories (e.g. using `apt-get` or -`yum`), and for other dependencies it sets up a virtualized Python -environment with packages downloaded from PyPI [#venv]_. It also -provides automated updates. - -To install and run the client, just type... - -.. code-block:: shell - - ./certbot-auto - -.. hint:: The Let's Encrypt servers enforce rate - limits on the number of certificates issued for one domain. It is recommended - to initially use the test server via `--test-cert` until you get the desired - certificates. - -Throughout the documentation, whenever you see references to -``certbot`` script/binary, you can substitute in -``certbot-auto``. For example, to get basic help you would type: - -.. code-block:: shell - - ./certbot-auto --help - -or for full help, type: - -.. code-block:: shell - - ./certbot-auto --help all - - -``certbot-auto`` is the recommended method of running the Certbot -client beta releases on systems that don't have a packaged version. Debian, -Arch Linux, Gentoo, FreeBSD, and OpenBSD now have native packages, so on those -systems you can just install ``certbot`` (and perhaps -``certbot-apache``). If you'd like to run the latest copy from Git, or -run your own locally modified copy of the client, follow the instructions in -the :doc:`contributing`. Some `other methods of installation`_ are discussed -below. +Many platforms now have native packages that give you a ``certbot`` or (for +older packages) ``letsencrypt`` command you can run. On others, the +``certbot-auto`` / ``letsencrypt-auto`` installer and wrapper script is a +stand-in. Throughout the documentation, whenever you see references to +``certbot`` script/binary, you should substitute in the name of the command +that certbot.eff.org_ told you to use on your system (``certbot``, +``letsencrypt``, or ``certbot-auto``). Plugins @@ -275,17 +247,21 @@ Certbot is working hard on improving the renewal process, and we apologize for any inconveniences you encounter in integrating these commands into your individual environment. +.. _command-line: + +Command line options +==================== + +Certbot supports a lot of command line options. Here's the full list, from +``certbot --help all``: + +.. literalinclude:: cli-help.txt .. _where-certs: Where are my certificates? ========================== -First of all, we encourage you to use Apache or nginx installers, both -which perform the certificate management automatically. If, however, -you prefer to manage everything by hand, this section provides -information on where to find necessary files. - All generated keys and issued certificates can be found in ``/etc/letsencrypt/live/$domain``. Rather than copying, please point your (web) server configuration directly to those files (or create @@ -391,7 +367,7 @@ give us as much information as possible: also might contain personally identifiable information) - copy and paste ``certbot --version`` output - your operating system, including specific version -- specify which installation_ method you've chosen +- specify which installation method you've chosen Other methods of installation ============================= From e85b387e4283d0577384ea6fa0184e6b0c801713 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Fri, 13 May 2016 13:21:49 -0700 Subject: [PATCH 102/396] Move 3rd party plugins back below others --- docs/using.rst | 74 ++++++++++++++++++++++++++++---------------------- 1 file changed, 41 insertions(+), 33 deletions(-) diff --git a/docs/using.rst b/docs/using.rst index 2c3465324..9371f44b4 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -82,39 +82,7 @@ manual_ Y N Helps you obtain a cert by giving you instructions to perf nginx_ Y Y Very experimental and not included in certbot-auto_. =========== ==== ==== =============================================================== -Third-party plugins -------------------- - -There are also a number of third-party plugins for the client, provided by other developers: - -=========== ==== ==== =============================================================== -Plugin Auth Inst Notes -=========== ==== ==== =============================================================== -plesk_ Y Y Integration with the Plesk web hosting tool -haproxy_ Y Y Integration with the HAProxy load balancer -s3front_ Y Y Integration with Amazon CloudFront distribution of S3 buckets -gandi_ Y Y Integration with Gandi's hosting products and API -varnish_ Y N Obtain certs via a Varnish server -external_ Y N A plugin for convenient scripting (See also ticket 2782_) -icecast_ N Y Deploy certs to Icecast 2 streaming media servers -pritunl_ N Y Install certs in pritunl distributed OpenVPN servers -proxmox_ N Y Install certs in Proxmox Virtualization servers -postfix_ N Y STARTTLS Everywhere is becoming a Certbot Postfix/Exim plugin -=========== ==== ==== =============================================================== - -.. _plesk: https://github.com/plesk/letsencrypt-plesk -.. _haproxy: https://code.greenhost.net/open/letsencrypt-haproxy -.. _s3front: https://github.com/dlapiduz/letsencrypt-s3front -.. _gandi: https://github.com/Gandi/letsencrypt-gandi -.. _icecast: https://github.com/e00E/lets-encrypt-icecast -.. _varnish: http://git.sesse.net/?p=letsencrypt-varnish-plugin -.. _2782: https://github.com/certbot/certbot/issues/2782 -.. _pritunl: https://github.com/kharkevich/letsencrypt-pritunl -.. _proxmox: https://github.com/kharkevich/letsencrypt-proxmox -.. _external: https://github.com/marcan/letsencrypt-external -.. _postfix: https://github.com/EFForg/starttls-everywhere - -If you're interested, you can also :ref:`write your own plugin `. +There are many third-party-plugins_ available. Apache ------ @@ -205,6 +173,46 @@ still experimental, however, and is not installed with certbot-auto_. If installed, you can select this plugin on the command line by including ``--nginx``. +.. _third-party-plugins: + +Third-party plugins +------------------- + +There are also a number of third-party plugins for the client, provided by +other developers. Many are beta/experimental, but some are already in +widespread use: + +=========== ==== ==== =============================================================== +Plugin Auth Inst Notes +=========== ==== ==== =============================================================== +plesk_ Y Y Integration with the Plesk web hosting tool +haproxy_ Y Y Integration with the HAProxy load balancer +s3front_ Y Y Integration with Amazon CloudFront distribution of S3 buckets +gandi_ Y Y Integration with Gandi's hosting products and API +varnish_ Y N Obtain certs via a Varnish server +external_ Y N A plugin for convenient scripting (See also ticket 2782_) +icecast_ N Y Deploy certs to Icecast 2 streaming media servers +pritunl_ N Y Install certs in pritunl distributed OpenVPN servers +proxmox_ N Y Install certs in Proxmox Virtualization servers +postfix_ N Y STARTTLS Everywhere is becoming a Certbot Postfix/Exim plugin +=========== ==== ==== =============================================================== + +.. _plesk: https://github.com/plesk/letsencrypt-plesk +.. _haproxy: https://code.greenhost.net/open/letsencrypt-haproxy +.. _s3front: https://github.com/dlapiduz/letsencrypt-s3front +.. _gandi: https://github.com/Gandi/letsencrypt-gandi +.. _icecast: https://github.com/e00E/lets-encrypt-icecast +.. _varnish: http://git.sesse.net/?p=letsencrypt-varnish-plugin +.. _2782: https://github.com/certbot/certbot/issues/2782 +.. _pritunl: https://github.com/kharkevich/letsencrypt-pritunl +.. _proxmox: https://github.com/kharkevich/letsencrypt-proxmox +.. _external: https://github.com/marcan/letsencrypt-external +.. _postfix: https://github.com/EFForg/starttls-everywhere + +If you're interested, you can also :ref:`write your own plugin `. + + + Renewal ======= From 1aff941ad090c7d7673b6f36bb5a2cc95cbdbad9 Mon Sep 17 00:00:00 2001 From: John Reed Date: Fri, 13 May 2016 18:37:43 -0500 Subject: [PATCH 103/396] Updating broken link to Google Python Style guide Old link: https://google-styleguide.googlecode.com/svn/trunk/pyguide.html New link: https://google.github.io/styleguide/pyguide.html --- docs/contributing.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/contributing.rst b/docs/contributing.rst index 49e9e146d..2ac38225c 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -297,7 +297,7 @@ Please: 4. Remember to use ``pylint``. .. _Google Python Style Guide: - https://google-styleguide.googlecode.com/svn/trunk/pyguide.html + https://google.github.io/styleguide/pyguide.html .. _Sphinx-style: http://sphinx-doc.org/ .. _PEP 8 - Style Guide for Python Code: https://www.python.org/dev/peps/pep-0008 From 3ddd97235642a5db872018a17b911620cfb58971 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Fri, 13 May 2016 18:19:57 -0700 Subject: [PATCH 104/396] Update the renewal-related message we print after obtaining a cert --- certbot/main.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/certbot/main.py b/certbot/main.py index 0405d6eb5..66804143c 100644 --- a/certbot/main.py +++ b/certbot/main.py @@ -97,7 +97,7 @@ def _auth_from_domains(le_client, config, domains, lineage=None): hooks.post_hook(config) if not config.dry_run and not config.verb == "renew": - _report_new_cert(lineage.cert, lineage.fullchain) + _report_new_cert(config, lineage.cert, lineage.fullchain) return lineage, action @@ -267,7 +267,7 @@ def _find_domains(config, installer): return domains -def _report_new_cert(cert_path, fullchain_path): +def _report_new_cert(config, cert_path, fullchain_path): """Reports the creation of a new certificate to the user. :param str cert_path: path to cert @@ -285,12 +285,15 @@ def _report_new_cert(cert_path, fullchain_path): # Unless we're in .csr mode and there really isn't one and_chain = "has " path = cert_path + + verbswitch = ' with the "certonly" option' if config.verb == "run" else "" # XXX Perhaps one day we could detect the presence of known old webservers # and say something more informative here. - msg = ("Congratulations! Your certificate {0} been saved at {1}." - " Your cert will expire on {2}. To obtain a new version of the " - "certificate in the future, simply run Certbot again." - .format(and_chain, path, expiry)) + msg = ('Congratulations! Your certificate {0} been saved at {1}.' + ' Your cert will expire on {2}. To obtain a new or tweaked version of this ' + 'certificate in the future, simply run {3} again{4}. ' + 'To non-interactively renew *all* of your ceriticates, run "{3} renew"' + .format(and_chain, path, expiry, cli.cli_command, verbswitch)) reporter_util.add_message(msg, reporter_util.MEDIUM_PRIORITY) @@ -485,7 +488,7 @@ def _csr_obtain_cert(config, le_client): else: cert_path, _, cert_fullchain = le_client.save_certificate( certr, chain, config.cert_path, config.chain_path, config.fullchain_path) - _report_new_cert(cert_path, cert_fullchain) + _report_new_cert(config, cert_path, cert_fullchain) def obtain_cert(config, plugins, lineage=None): From f4103bdbb3163c6e62de5e5356d0b77a6d902d9e Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Fri, 13 May 2016 18:49:01 -0700 Subject: [PATCH 105/396] post-hook only runs if pre-hook was (or would have been, if it existed) --- certbot/cli.py | 3 ++- certbot/hooks.py | 7 ++++++- certbot/main.py | 2 +- certbot/tests/hook_test.py | 8 ++++++++ 4 files changed, 17 insertions(+), 3 deletions(-) diff --git a/certbot/cli.py b/certbot/cli.py index d4695ba4d..41d31fa35 100644 --- a/certbot/cli.py +++ b/certbot/cli.py @@ -760,7 +760,8 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): "renew", "--post-hook", help="Command to be run in a shell after attempting to obtain/renew " " certificates. Can be used to deploy renewed certificates, or to restart" - " any servers that were stopped by --pre-hook.") + " any servers that were stopped by --pre-hook. This is only run if " + " an attempt was made to obtain/renew a certificate.") helpful.add( "renew", "--renew-hook", help="Command to be run in a shell once for each successfully renewed certificate." diff --git a/certbot/hooks.py b/certbot/hooks.py index 138e2addc..f5c2e47ae 100644 --- a/certbot/hooks.py +++ b/certbot/hooks.py @@ -39,7 +39,7 @@ def pre_hook(config): if config.pre_hook and not pre_hook.already: logger.info("Running pre-hook command: %s", config.pre_hook) _run_hook(config.pre_hook) - pre_hook.already = True + pre_hook.already = True pre_hook.already = False @@ -50,6 +50,11 @@ def post_hook(config, final=False): we're called with final=True before actually doing anything. """ if config.post_hook: + if not pre_hook.already: + logger.info("No renewals attempted, so not running post-hook") + if config.verb != "renew": + logger.warn("Sanity failure in renewal hooks") + return if final or config.verb != "renew": logger.info("Running post-hook command: %s", config.post_hook) _run_hook(config.post_hook) diff --git a/certbot/main.py b/certbot/main.py index 0405d6eb5..548243bf6 100644 --- a/certbot/main.py +++ b/certbot/main.py @@ -94,7 +94,7 @@ def _auth_from_domains(le_client, config, domains, lineage=None): if lineage is False: raise errors.Error("Certificate could not be obtained") finally: - hooks.post_hook(config) + hooks.post_hook(config, final=False) if not config.dry_run and not config.verb == "renew": _report_new_cert(lineage.cert, lineage.fullchain) diff --git a/certbot/tests/hook_test.py b/certbot/tests/hook_test.py index ce78b5dc9..be7fb852d 100644 --- a/certbot/tests/hook_test.py +++ b/certbot/tests/hook_test.py @@ -56,14 +56,22 @@ class HookTest(unittest.TestCase): return mock_logger.warning def test_pre_hook(self): + hooks.pre_hook.already = False config = mock.MagicMock(pre_hook="true") self._test_a_hook(config, hooks.pre_hook, 1) config = mock.MagicMock(pre_hook="") self._test_a_hook(config, hooks.pre_hook, 0) def test_post_hook(self): + hooks.pre_hook.already = False + # if pre-hook isn't called, post-hook shouldn't be config = mock.MagicMock(post_hook="true", verb="splonk") + self._test_a_hook(config, hooks.post_hook, 0) + + config = mock.MagicMock(post_hook="true", verb="splonk") + self._test_a_hook(config, hooks.pre_hook, 1) self._test_a_hook(config, hooks.post_hook, 2) + config = mock.MagicMock(post_hook="true", verb="renew") self._test_a_hook(config, hooks.post_hook, 0) From 4cb35eaeb346e11963c59fd4aa5170a7e01f7190 Mon Sep 17 00:00:00 2001 From: Tapple Gao Date: Sun, 15 May 2016 11:44:48 +0200 Subject: [PATCH 106/396] system python path has changed on el capitan. Look for both old and new path --- letsencrypt-auto-source/pieces/bootstrappers/mac.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/letsencrypt-auto-source/pieces/bootstrappers/mac.sh b/letsencrypt-auto-source/pieces/bootstrappers/mac.sh index e41db04b1..2b04977c8 100755 --- a/letsencrypt-auto-source/pieces/bootstrappers/mac.sh +++ b/letsencrypt-auto-source/pieces/bootstrappers/mac.sh @@ -16,7 +16,8 @@ BootstrapMac() { $pkgcmd augeas $pkgcmd dialog - if [ "$(which python)" = "/System/Library/Frameworks/Python.framework/Versions/2.7/bin/python" ]; then + if [ "$(which python)" = "/System/Library/Frameworks/Python.framework/Versions/2.7/bin/python" \ + -o "$(which python)" = "/usr/bin/python" ]; then # We want to avoid using the system Python because it requires root to use pip. # python.org, MacPorts or HomeBrew Python installations should all be OK. echo "Installing python..." From 8f696b3ad77dea64b19510bf084af28ea5f5a0b2 Mon Sep 17 00:00:00 2001 From: Jacob Hoffman-Andrews Date: Sun, 15 May 2016 13:48:51 -0700 Subject: [PATCH 107/396] Reuse HTTP connections. (#2855) Fixes #2778 --- acme/acme/client.py | 6 +++- acme/acme/client_test.py | 63 +++++++++++++++++++++++----------------- 2 files changed, 42 insertions(+), 27 deletions(-) diff --git a/acme/acme/client.py b/acme/acme/client.py index 225245686..117ee6b7d 100644 --- a/acme/acme/client.py +++ b/acme/acme/client.py @@ -512,6 +512,10 @@ class ClientNetwork(object): # pylint: disable=too-many-instance-attributes self.verify_ssl = verify_ssl self._nonces = set() self.user_agent = user_agent + self.session = requests.Session() + + def __del__(self): + self.session.close() def _wrap_in_jws(self, obj, nonce): """Wrap `JSONDeSerializable` object in JWS. @@ -606,7 +610,7 @@ class ClientNetwork(object): # pylint: disable=too-many-instance-attributes kwargs['verify'] = self.verify_ssl kwargs.setdefault('headers', {}) kwargs['headers'].setdefault('User-Agent', self.user_agent) - response = requests.request(method, url, *args, **kwargs) + response = self.session.request(method, url, *args, **kwargs) logging.debug('Received %s. Headers: %s. Content: %r', response, response.headers, response.content) return response diff --git a/acme/acme/client_test.py b/acme/acme/client_test.py index 7403cde81..33e80aab7 100644 --- a/acme/acme/client_test.py +++ b/acme/acme/client_test.py @@ -530,40 +530,49 @@ class ClientNetworkTest(unittest.TestCase): self.assertEqual( self.response, self.net._check_response(self.response)) - @mock.patch('acme.client.requests') - def test_send_request(self, mock_requests): - mock_requests.request.return_value = self.response + def test_send_request(self): + self.net.session = mock.MagicMock() + self.net.session.request.return_value = self.response # pylint: disable=protected-access self.assertEqual(self.response, self.net._send_request( - 'HEAD', 'url', 'foo', bar='baz')) - mock_requests.request.assert_called_once_with( - 'HEAD', 'url', 'foo', verify=mock.ANY, bar='baz', headers=mock.ANY) + 'HEAD', 'http://example.com/', 'foo', bar='baz')) + self.net.session.request.assert_called_once_with( + 'HEAD', 'http://example.com/', 'foo', + headers=mock.ANY, verify=mock.ANY, bar='baz') - @mock.patch('acme.client.requests') - def test_send_request_verify_ssl(self, mock_requests): + def test_send_request_verify_ssl(self): # pylint: disable=protected-access for verify in True, False: - mock_requests.request.reset_mock() - mock_requests.request.return_value = self.response + self.net.session = mock.MagicMock() + self.net.session.request.return_value = self.response self.net.verify_ssl = verify # pylint: disable=protected-access self.assertEqual( - self.response, self.net._send_request('GET', 'url')) - mock_requests.request.assert_called_once_with( - 'GET', 'url', verify=verify, headers=mock.ANY) + self.response, + self.net._send_request('GET', 'http://example.com/')) + self.net.session.request.assert_called_once_with( + 'GET', 'http://example.com/', verify=verify, headers=mock.ANY) - @mock.patch('acme.client.requests') - def test_send_request_user_agent(self, mock_requests): - mock_requests.request.return_value = self.response + def test_send_request_user_agent(self): + self.net.session = mock.MagicMock() # pylint: disable=protected-access - self.net._send_request('GET', 'url', headers={'bar': 'baz'}) - mock_requests.request.assert_called_once_with( - 'GET', 'url', verify=mock.ANY, + self.net._send_request('GET', 'http://example.com/', + headers={'bar': 'baz'}) + self.net.session.request.assert_called_once_with( + 'GET', 'http://example.com/', verify=mock.ANY, headers={'User-Agent': 'acme-python-test', 'bar': 'baz'}) - self.net._send_request('GET', 'url', headers={'User-Agent': 'foo2'}) - mock_requests.request.assert_called_with( - 'GET', 'url', verify=mock.ANY, headers={'User-Agent': 'foo2'}) + self.net._send_request('GET', 'http://example.com/', + headers={'User-Agent': 'foo2'}) + self.net.session.request.assert_called_with( + 'GET', 'http://example.com/', + verify=mock.ANY, headers={'User-Agent': 'foo2'}) + + def test_del(self): + sess = mock.MagicMock() + self.net.session = sess + del self.net + sess.close.assert_called_once() @mock.patch('acme.client.requests') def test_requests_error_passthrough(self, mock_requests): @@ -616,14 +625,16 @@ class ClientNetworkWithMockedResponseTest(unittest.TestCase): return self.checked_response def test_head(self): - self.assertEqual(self.response, self.net.head('url', 'foo', bar='baz')) + self.assertEqual(self.response, self.net.head( + 'http://example.com/', 'foo', bar='baz')) self.send_request.assert_called_once_with( - 'HEAD', 'url', 'foo', bar='baz') + 'HEAD', 'http://example.com/', 'foo', bar='baz') def test_get(self): self.assertEqual(self.checked_response, self.net.get( - 'url', content_type=self.content_type, bar='baz')) - self.send_request.assert_called_once_with('GET', 'url', bar='baz') + 'http://example.com/', content_type=self.content_type, bar='baz')) + self.send_request.assert_called_once_with( + 'GET', 'http://example.com/', bar='baz') def test_post(self): # pylint: disable=protected-access From f092669347291463047707551a736b21c8200de9 Mon Sep 17 00:00:00 2001 From: sagi Date: Mon, 16 May 2016 21:19:07 +0000 Subject: [PATCH 108/396] If cert_path provided - do not randomize it --- certbot/client.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/certbot/client.py b/certbot/client.py index 6f41a3a0b..fda7707f6 100644 --- a/certbot/client.py +++ b/certbot/client.py @@ -317,7 +317,13 @@ class Client(object): cert_pem = OpenSSL.crypto.dump_certificate( OpenSSL.crypto.FILETYPE_PEM, certr.body.wrapped) - cert_file, act_cert_path = le_util.unique_file(cert_path, 0o644) + + if cert_path != constants.CLI_DEFAULTS['auth_cert_path']: + cert_file = le_util.safe_open(cert_path, chmod=0o644) + act_cert_path = cert_path + else: + cert_file, act_cert_path = le_util.unique_file(cert_path, 0o644) + try: cert_file.write(cert_pem) finally: From d39dee20ad9c0c4ddde31bdaeeb35b49135e902d Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 16 May 2016 15:06:51 -0700 Subject: [PATCH 109/396] fix auto arg parsing --- letsencrypt-auto-source/letsencrypt-auto | 25 ++++++++++--------- .../letsencrypt-auto.template | 22 ++++++++-------- 2 files changed, 24 insertions(+), 23 deletions(-) diff --git a/letsencrypt-auto-source/letsencrypt-auto b/letsencrypt-auto-source/letsencrypt-auto index b255a99a7..bbb2cda54 100755 --- a/letsencrypt-auto-source/letsencrypt-auto +++ b/letsencrypt-auto-source/letsencrypt-auto @@ -38,17 +38,6 @@ Help for certbot itself cannot be provided until it is installed. All arguments are accepted and forwarded to the Certbot client when run." -while getopts ":hnv" arg; do - case $arg in - h) - HELP=1;; - n) - ASSUME_YES=1;; - v) - VERBOSE=1;; - esac -done - for arg in "$@" ; do case "$arg" in --debug) @@ -65,6 +54,17 @@ for arg in "$@" ; do ASSUME_YES=1;; --verbose) VERBOSE=1;; + -[!-]*) + while getopts ":hnv" short_arg $arg; do + case "$short_arg" in + h) + HELP=1;; + n) + ASSUME_YES=1;; + v) + VERBOSE=1;; + esac + done;; esac done @@ -435,7 +435,8 @@ BootstrapMac() { # Workaround for _dlopen not finding augeas on OS X if [ "$pkgman" = "port" ] && ! [ -e "/usr/local/lib/libaugeas.dylib" ] && [ -e "/opt/local/lib/libaugeas.dylib" ]; then echo "Applying augeas workaround" - $SUDO ln -s /opt/local/lib/libaugeas.dylib /usr/local/lib + $SUDO mkdir -p /usr/local/lib/ + $SUDO ln -s /opt/local/lib/libaugeas.dylib /usr/local/lib/ fi if ! hash pip 2>/dev/null; then diff --git a/letsencrypt-auto-source/letsencrypt-auto.template b/letsencrypt-auto-source/letsencrypt-auto.template index 33b140bca..f24746b46 100755 --- a/letsencrypt-auto-source/letsencrypt-auto.template +++ b/letsencrypt-auto-source/letsencrypt-auto.template @@ -38,17 +38,6 @@ Help for certbot itself cannot be provided until it is installed. All arguments are accepted and forwarded to the Certbot client when run." -while getopts ":hnv" arg; do - case $arg in - h) - HELP=1;; - n) - ASSUME_YES=1;; - v) - VERBOSE=1;; - esac -done - for arg in "$@" ; do case "$arg" in --debug) @@ -65,6 +54,17 @@ for arg in "$@" ; do ASSUME_YES=1;; --verbose) VERBOSE=1;; + -[!-]*) + while getopts ":hnv" short_arg $arg; do + case "$short_arg" in + h) + HELP=1;; + n) + ASSUME_YES=1;; + v) + VERBOSE=1;; + esac + done;; esac done From c0228ef1aa505a84a87529f76177f7dc6aa51214 Mon Sep 17 00:00:00 2001 From: sagi Date: Mon, 16 May 2016 22:11:15 +0000 Subject: [PATCH 110/396] Boulder integration scripts provides a cert_path --- tests/boulder-integration.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/boulder-integration.sh b/tests/boulder-integration.sh index 201343525..a1245e1c9 100755 --- a/tests/boulder-integration.sh +++ b/tests/boulder-integration.sh @@ -43,7 +43,7 @@ export CSR_PATH="${root}/csr.der" KEY_PATH="${root}/key.pem" \ common auth --csr "$CSR_PATH" \ --cert-path "${root}/csr/cert.pem" \ --chain-path "${root}/csr/chain.pem" -openssl x509 -in "${root}/csr/0000_cert.pem" -text +openssl x509 -in "${root}/csr/cert.pem" -text openssl x509 -in "${root}/csr/0000_chain.pem" -text common --domains le3.wtf install \ From 9efdd3b38fa0de68cd01930c9adec7728a372916 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 16 May 2016 17:34:30 -0700 Subject: [PATCH 111/396] Fixes 2977 --- certbot/renewal.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/certbot/renewal.py b/certbot/renewal.py index 3682c50d5..7e0da6afa 100644 --- a/certbot/renewal.py +++ b/certbot/renewal.py @@ -301,7 +301,8 @@ def _renew_describe_results(config, renew_successes, renew_failures, def renew_all_lineages(config): """Examine each lineage; renew if due and report results""" - if config.domains != []: + if (config.domains != [] and + set(config.domains) != six.viewkeys(config.webroot_map)): raise errors.Error("Currently, the renew verb is only capable of " "renewing all installed certificates that are due " "to be renewed; individual domains cannot be " From 323bb34144df2b77739dd29b475acac553d32d36 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 16 May 2016 17:45:52 -0700 Subject: [PATCH 112/396] Add test to prevent regressions --- certbot/tests/cli_test.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/certbot/tests/cli_test.py b/certbot/tests/cli_test.py index 31056cafe..d7965a24e 100644 --- a/certbot/tests/cli_test.py +++ b/certbot/tests/cli_test.py @@ -712,6 +712,12 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods self._test_renew_common(renewalparams=renewalparams, assert_oc_called=True) + def test_renew_with_webroot_map(self): + renewalparams = {'authenticator': 'webroot'} + self._test_renew_common( + renewalparams=renewalparams, assert_oc_called=True, + args=['renew', '--webroot-map', '{"example.com": "/tmp"}']) + def test_renew_reconstitute_error(self): # pylint: disable=protected-access with mock.patch('certbot.main.renewal._reconstitute') as mock_reconstitute: From b9c97954eeaabb51d36260a236eb7f562bad8729 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 16 May 2016 17:48:26 -0700 Subject: [PATCH 113/396] Add comment about removing the exception in the future --- certbot/renewal.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/certbot/renewal.py b/certbot/renewal.py index 7e0da6afa..24cd67c78 100644 --- a/certbot/renewal.py +++ b/certbot/renewal.py @@ -301,6 +301,8 @@ def _renew_describe_results(config, renew_successes, renew_failures, def renew_all_lineages(config): """Examine each lineage; renew if due and report results""" + # If more plugins start using cli.add_domains, + # we may want to only log a warning here if (config.domains != [] and set(config.domains) != six.viewkeys(config.webroot_map)): raise errors.Error("Currently, the renew verb is only capable of " From accc83a1ca9dd7dcf8c815a8f5b24788ba94bb73 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 16 May 2016 18:03:17 -0700 Subject: [PATCH 114/396] add py2.6 compatibility --- certbot/renewal.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/certbot/renewal.py b/certbot/renewal.py index 24cd67c78..b5b982972 100644 --- a/certbot/renewal.py +++ b/certbot/renewal.py @@ -301,10 +301,10 @@ def _renew_describe_results(config, renew_successes, renew_failures, def renew_all_lineages(config): """Examine each lineage; renew if due and report results""" - # If more plugins start using cli.add_domains, - # we may want to only log a warning here - if (config.domains != [] and - set(config.domains) != six.viewkeys(config.webroot_map)): + # This is trivially False if config.domains is empty + if any(domain not in config.webroot_map for domain in config.domains): + # If more plugins start using cli.add_domains, + # we may want to only log a warning here raise errors.Error("Currently, the renew verb is only capable of " "renewing all installed certificates that are due " "to be renewed; individual domains cannot be " From 3cf3e5b685753b353c2afa42fd2472a61790b44a Mon Sep 17 00:00:00 2001 From: sagi Date: Tue, 17 May 2016 17:52:29 +0000 Subject: [PATCH 115/396] Detect RewriteEngine directives that originate in VirtualHosts --- certbot-apache/certbot_apache/configurator.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/certbot-apache/certbot_apache/configurator.py b/certbot-apache/certbot_apache/configurator.py index 26c3185be..cc269ff62 100644 --- a/certbot-apache/certbot_apache/configurator.py +++ b/certbot-apache/certbot_apache/configurator.py @@ -1177,10 +1177,14 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): :type vhost: :class:`~certbot_apache.obj.VirtualHost` """ - rewrite_engine_path = self.parser.find_dir("RewriteEngine", "on", + rewrite_engine_path_list= self.parser.find_dir("RewriteEngine", "on", start=vhost.path) - if rewrite_engine_path: - return self.parser.get_arg(rewrite_engine_path[0]) + if rewrite_engine_path_list: + for re_path in rewrite_engine_path_list: + # A RewriteEngine directive may also be included in per + # directory .htaccess files. We only care about the VirtualHost. + if 'VirtualHost' in re_path: + return self.parser.get_arg(re_path) return False def _create_redirect_vhost(self, ssl_vhost): From 886776d7415ec9fd2e33cfa57a9047f99da74bfc Mon Sep 17 00:00:00 2001 From: sagi Date: Tue, 17 May 2016 18:29:39 +0000 Subject: [PATCH 116/396] Make lint happy --- certbot-apache/certbot_apache/configurator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certbot-apache/certbot_apache/configurator.py b/certbot-apache/certbot_apache/configurator.py index cc269ff62..12125d522 100644 --- a/certbot-apache/certbot_apache/configurator.py +++ b/certbot-apache/certbot_apache/configurator.py @@ -1177,7 +1177,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): :type vhost: :class:`~certbot_apache.obj.VirtualHost` """ - rewrite_engine_path_list= self.parser.find_dir("RewriteEngine", "on", + rewrite_engine_path_list = self.parser.find_dir("RewriteEngine", "on", start=vhost.path) if rewrite_engine_path_list: for re_path in rewrite_engine_path_list: From 85e962455559e25933cdf4ef7ecf8d37c8a03bde Mon Sep 17 00:00:00 2001 From: chrismarget Date: Tue, 17 May 2016 19:50:57 +0000 Subject: [PATCH 117/396] Added test for random certificate serial numbers from gen_ss_cert. --- acme/acme/crypto_util_test.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/acme/acme/crypto_util_test.py b/acme/acme/crypto_util_test.py index 147cd5a2a..b41243b8f 100644 --- a/acme/acme/crypto_util_test.py +++ b/acme/acme/crypto_util_test.py @@ -8,6 +8,8 @@ import unittest import six from six.moves import socketserver # pylint: disable=import-error +import OpenSSL + from acme import errors from acme import jose from acme import test_util @@ -126,5 +128,23 @@ class PyOpenSSLCertOrReqSANTest(unittest.TestCase): self._get_idn_names()) +class RandomSnTest(unittest.TestCase): + """Test for random certificate serial numbers.""" + + def setUp(self): + self.certCount = 5 + self.serialNum = [] + self.key = OpenSSL.crypto.PKey() + self.key.generate_key(OpenSSL.crypto.TYPE_RSA, 2048) + + def test_sn_collisions(self): + from acme.crypto_util import gen_ss_cert + + for _ in range(self.certCount): + cert = gen_ss_cert(self.key, ['dummy'], force_san=True) + self.serialNum.append(cert.get_serial_number()) + self.assertTrue(len(set(self.serialNum)) > 1) + + if __name__ == '__main__': unittest.main() # pragma: no cover From 6dd99913719df02d5fbee28952774cdbf16a596e Mon Sep 17 00:00:00 2001 From: chrismarget Date: Tue, 17 May 2016 20:10:20 +0000 Subject: [PATCH 118/396] Fix invalid attribute for pylint --- acme/acme/crypto_util_test.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/acme/acme/crypto_util_test.py b/acme/acme/crypto_util_test.py index b41243b8f..75a908d4f 100644 --- a/acme/acme/crypto_util_test.py +++ b/acme/acme/crypto_util_test.py @@ -132,18 +132,18 @@ class RandomSnTest(unittest.TestCase): """Test for random certificate serial numbers.""" def setUp(self): - self.certCount = 5 - self.serialNum = [] + self.cert_count = 5 + self.serial_num = [] self.key = OpenSSL.crypto.PKey() self.key.generate_key(OpenSSL.crypto.TYPE_RSA, 2048) def test_sn_collisions(self): from acme.crypto_util import gen_ss_cert - for _ in range(self.certCount): + for _ in range(self.cert_count): cert = gen_ss_cert(self.key, ['dummy'], force_san=True) - self.serialNum.append(cert.get_serial_number()) - self.assertTrue(len(set(self.serialNum)) > 1) + self.serial_num.append(cert.get_serial_number()) + self.assertTrue(len(set(self.serial_num)) > 1) if __name__ == '__main__': From 7e3c9399e54f2fb4960296e6211595707ff2dcf5 Mon Sep 17 00:00:00 2001 From: sagi Date: Tue, 17 May 2016 22:12:11 +0000 Subject: [PATCH 119/396] Use cli.set_by_cli to detect if the user explicitly set cert_path --- certbot/client.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/certbot/client.py b/certbot/client.py index fda7707f6..dd38e47b0 100644 --- a/certbot/client.py +++ b/certbot/client.py @@ -24,6 +24,7 @@ from certbot import interfaces from certbot import le_util from certbot import reverter from certbot import storage +from certbot import cli from certbot.display import ops as display_ops from certbot.display import enhancements @@ -318,7 +319,7 @@ class Client(object): cert_pem = OpenSSL.crypto.dump_certificate( OpenSSL.crypto.FILETYPE_PEM, certr.body.wrapped) - if cert_path != constants.CLI_DEFAULTS['auth_cert_path']: + if cli.set_by_cli('cert_path'): cert_file = le_util.safe_open(cert_path, chmod=0o644) act_cert_path = cert_path else: From 12a0312282da5b22d67d0048d2a4bf79739b0a5b Mon Sep 17 00:00:00 2001 From: Amjad Mashaal Date: Thu, 5 May 2016 09:22:50 +0200 Subject: [PATCH 120/396] Fixing auto_test.py for Python 2.6 --- letsencrypt-auto-source/tests/auto_test.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/letsencrypt-auto-source/tests/auto_test.py b/letsencrypt-auto-source/tests/auto_test.py index 3b7e8731b..fa265c1c0 100644 --- a/letsencrypt-auto-source/tests/auto_test.py +++ b/letsencrypt-auto-source/tests/auto_test.py @@ -11,7 +11,7 @@ from shutil import copy, rmtree import socket import ssl from stat import S_IRUSR, S_IXUSR -from subprocess import CalledProcessError, check_output, Popen, PIPE +from subprocess import CalledProcessError, Popen, PIPE import sys from tempfile import mkdtemp from threading import Thread @@ -146,7 +146,7 @@ def out_and_err(command, input=None, shell=False, env=None): out, err = process.communicate(input=input) status = process.poll() # same as in check_output(), though wait() sounds better if status: - raise CalledProcessError(status, command, output=out) + raise CalledProcessError(status, command) return out, err From d57c9434710a2bd1d74dd382420da6caf9781974 Mon Sep 17 00:00:00 2001 From: Amjad Mashaal Date: Thu, 5 May 2016 10:08:28 +0200 Subject: [PATCH 121/396] Fixing broken tests --- letsencrypt-auto-source/tests/auto_test.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/letsencrypt-auto-source/tests/auto_test.py b/letsencrypt-auto-source/tests/auto_test.py index fa265c1c0..7f0b31b67 100644 --- a/letsencrypt-auto-source/tests/auto_test.py +++ b/letsencrypt-auto-source/tests/auto_test.py @@ -146,7 +146,9 @@ def out_and_err(command, input=None, shell=False, env=None): out, err = process.communicate(input=input) status = process.poll() # same as in check_output(), though wait() sounds better if status: - raise CalledProcessError(status, command) + error = CalledProcessError(status, command) + error.output = out + raise error return out, err From 14778c15cef825e94255fd8b6913186eb977dbd0 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Tue, 17 May 2016 20:05:47 -0700 Subject: [PATCH 122/396] Run build to make le-auto up to date --- letsencrypt-auto-source/letsencrypt-auto | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/letsencrypt-auto-source/letsencrypt-auto b/letsencrypt-auto-source/letsencrypt-auto index bbb2cda54..0f309243b 100755 --- a/letsencrypt-auto-source/letsencrypt-auto +++ b/letsencrypt-auto-source/letsencrypt-auto @@ -452,6 +452,11 @@ BootstrapMac() { fi } +BootstrapSmartOS() { + pkgin update + pkgin -y install 'gcc49' 'py27-augeas' 'py27-virtualenv' +} + # Install required OS packages: Bootstrap() { @@ -484,8 +489,10 @@ Bootstrap() { ExperimentalBootstrap "FreeBSD" BootstrapFreeBsd elif uname | grep -iq Darwin ; then ExperimentalBootstrap "Mac OS X" BootstrapMac - elif grep -iq "Amazon Linux" /etc/issue ; then + elif [ -f /etc/issue ] && grep -iq "Amazon Linux" /etc/issue ; then ExperimentalBootstrap "Amazon Linux" BootstrapRpmCommon + elif [ -f /etc/product ] && grep -q "Joyent Instance" /etc/product ; then + ExperimentalBootstrap "Joyent SmartOS Zone" BootstrapSmartOS else echo "Sorry, I don't know how to bootstrap Certbot on your operating system!" echo From af41345967ec5a9a00caf10d76b7537bef359f4f Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Tue, 17 May 2016 20:06:35 -0700 Subject: [PATCH 123/396] Put arg parsing in one place --- letsencrypt-auto-source/letsencrypt-auto | 12 ++++++------ letsencrypt-auto-source/letsencrypt-auto.template | 12 ++++++------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/letsencrypt-auto-source/letsencrypt-auto b/letsencrypt-auto-source/letsencrypt-auto index 0f309243b..a85ca7695 100755 --- a/letsencrypt-auto-source/letsencrypt-auto +++ b/letsencrypt-auto-source/letsencrypt-auto @@ -68,6 +68,12 @@ for arg in "$@" ; do esac done +if [ $BASENAME = "letsencrypt-auto" ]; then + # letsencrypt-auto does not respect --help or --yes for backwards compatibility + ASSUME_YES=1 + HELP=0 +fi + # certbot-auto needs root access to bootstrap OS dependencies, and # certbot itself needs root access for almost all modes of operation # The "normal" case is that sudo is used for the steps that need root, but @@ -107,12 +113,6 @@ else SUDO= fi -if [ $BASENAME = "letsencrypt-auto" ]; then - # letsencrypt-auto does not respect --help or --yes for backwards compatibility - ASSUME_YES=1 - HELP=0 -fi - ExperimentalBootstrap() { # Arguments: Platform name, bootstrap function name if [ "$DEBUG" = 1 ]; then diff --git a/letsencrypt-auto-source/letsencrypt-auto.template b/letsencrypt-auto-source/letsencrypt-auto.template index 5a4ddee7d..62624a5a8 100755 --- a/letsencrypt-auto-source/letsencrypt-auto.template +++ b/letsencrypt-auto-source/letsencrypt-auto.template @@ -68,6 +68,12 @@ for arg in "$@" ; do esac done +if [ $BASENAME = "letsencrypt-auto" ]; then + # letsencrypt-auto does not respect --help or --yes for backwards compatibility + ASSUME_YES=1 + HELP=0 +fi + # certbot-auto needs root access to bootstrap OS dependencies, and # certbot itself needs root access for almost all modes of operation # The "normal" case is that sudo is used for the steps that need root, but @@ -107,12 +113,6 @@ else SUDO= fi -if [ $BASENAME = "letsencrypt-auto" ]; then - # letsencrypt-auto does not respect --help or --yes for backwards compatibility - ASSUME_YES=1 - HELP=0 -fi - ExperimentalBootstrap() { # Arguments: Platform name, bootstrap function name if [ "$DEBUG" = 1 ]; then From 45b7c407c17bed5403f1245fd549f2ca0ebafb89 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Tue, 17 May 2016 20:07:06 -0700 Subject: [PATCH 124/396] Don't tell people you check for updates on every run --- letsencrypt-auto-source/letsencrypt-auto | 1 - letsencrypt-auto-source/letsencrypt-auto.template | 1 - 2 files changed, 2 deletions(-) diff --git a/letsencrypt-auto-source/letsencrypt-auto b/letsencrypt-auto-source/letsencrypt-auto index a85ca7695..a6f15b552 100755 --- a/letsencrypt-auto-source/letsencrypt-auto +++ b/letsencrypt-auto-source/letsencrypt-auto @@ -931,7 +931,6 @@ else fi if [ "$NO_SELF_UPGRADE" != 1 ]; then - echo "Checking for new version..." TEMP_DIR=$(TempDir) # --------------------------------------------------------------------------- cat << "UNLIKELY_EOF" > "$TEMP_DIR/fetch.py" diff --git a/letsencrypt-auto-source/letsencrypt-auto.template b/letsencrypt-auto-source/letsencrypt-auto.template index 62624a5a8..ca9dfc289 100755 --- a/letsencrypt-auto-source/letsencrypt-auto.template +++ b/letsencrypt-auto-source/letsencrypt-auto.template @@ -288,7 +288,6 @@ else fi if [ "$NO_SELF_UPGRADE" != 1 ]; then - echo "Checking for new version..." TEMP_DIR=$(TempDir) # --------------------------------------------------------------------------- cat << "UNLIKELY_EOF" > "$TEMP_DIR/fetch.py" From 502eba1cc41e10dc196ff936b3d8f0c639e4a41d Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Tue, 17 May 2016 20:07:45 -0700 Subject: [PATCH 125/396] Simplify SUDO certbot prompt --- letsencrypt-auto-source/letsencrypt-auto | 3 +-- letsencrypt-auto-source/letsencrypt-auto.template | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/letsencrypt-auto-source/letsencrypt-auto b/letsencrypt-auto-source/letsencrypt-auto index a6f15b552..306dacdf2 100755 --- a/letsencrypt-auto-source/letsencrypt-auto +++ b/letsencrypt-auto-source/letsencrypt-auto @@ -899,13 +899,12 @@ UNLIKELY_EOF echo "Installation succeeded." fi echo "Requesting root privileges to run certbot..." + echo " $VENV_BIN/letsencrypt" "$@" if [ -z "$SUDO_ENV" ] ; then # SUDO is su wrapper / noop - echo " " $SUDO "$VENV_BIN/letsencrypt" "$@" $SUDO "$VENV_BIN/letsencrypt" "$@" else # sudo - echo " " $SUDO "$SUDO_ENV" "$VENV_BIN/letsencrypt" "$@" $SUDO "$SUDO_ENV" "$VENV_BIN/letsencrypt" "$@" fi diff --git a/letsencrypt-auto-source/letsencrypt-auto.template b/letsencrypt-auto-source/letsencrypt-auto.template index ca9dfc289..bbba45f21 100755 --- a/letsencrypt-auto-source/letsencrypt-auto.template +++ b/letsencrypt-auto-source/letsencrypt-auto.template @@ -256,13 +256,12 @@ UNLIKELY_EOF echo "Installation succeeded." fi echo "Requesting root privileges to run certbot..." + echo " $VENV_BIN/letsencrypt" "$@" if [ -z "$SUDO_ENV" ] ; then # SUDO is su wrapper / noop - echo " " $SUDO "$VENV_BIN/letsencrypt" "$@" $SUDO "$VENV_BIN/letsencrypt" "$@" else # sudo - echo " " $SUDO "$SUDO_ENV" "$VENV_BIN/letsencrypt" "$@" $SUDO "$SUDO_ENV" "$VENV_BIN/letsencrypt" "$@" fi From 507b1542760ca17443fb86363f51e83238f23e36 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Tue, 17 May 2016 20:11:02 -0700 Subject: [PATCH 126/396] Don't saying you're requesting root unless you really are --- letsencrypt-auto-source/letsencrypt-auto | 7 +++++-- letsencrypt-auto-source/letsencrypt-auto.template | 7 +++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/letsencrypt-auto-source/letsencrypt-auto b/letsencrypt-auto-source/letsencrypt-auto index 306dacdf2..659600cdd 100755 --- a/letsencrypt-auto-source/letsencrypt-auto +++ b/letsencrypt-auto-source/letsencrypt-auto @@ -898,8 +898,11 @@ UNLIKELY_EOF fi echo "Installation succeeded." fi - echo "Requesting root privileges to run certbot..." - echo " $VENV_BIN/letsencrypt" "$@" + if [ -n "$SUDO" ]; then + # SUDO is su wrapper or sudo + echo "Requesting root privileges to run certbot..." + echo " $VENV_BIN/letsencrypt" "$@" + fi if [ -z "$SUDO_ENV" ] ; then # SUDO is su wrapper / noop $SUDO "$VENV_BIN/letsencrypt" "$@" diff --git a/letsencrypt-auto-source/letsencrypt-auto.template b/letsencrypt-auto-source/letsencrypt-auto.template index bbba45f21..f1ed82c4c 100755 --- a/letsencrypt-auto-source/letsencrypt-auto.template +++ b/letsencrypt-auto-source/letsencrypt-auto.template @@ -255,8 +255,11 @@ UNLIKELY_EOF fi echo "Installation succeeded." fi - echo "Requesting root privileges to run certbot..." - echo " $VENV_BIN/letsencrypt" "$@" + if [ -n "$SUDO" ]; then + # SUDO is su wrapper or sudo + echo "Requesting root privileges to run certbot..." + echo " $VENV_BIN/letsencrypt" "$@" + fi if [ -z "$SUDO_ENV" ] ; then # SUDO is su wrapper / noop $SUDO "$VENV_BIN/letsencrypt" "$@" From 63c79f98ca44453c3fe68c37ddcacb5b7bed938c Mon Sep 17 00:00:00 2001 From: Marius Gedminas Date: Wed, 18 May 2016 10:59:58 +0300 Subject: [PATCH 127/396] Remove dangling footnote This footnote has no references! --- docs/using.rst | 7 ------- 1 file changed, 7 deletions(-) diff --git a/docs/using.rst b/docs/using.rst index 12ea3ea11..e81aca16a 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -552,10 +552,3 @@ Beyond the methods discussed here, other methods may be possible, such as installing Certbot directly with pip from PyPI or downloading a ZIP archive from GitHub may be technically possible but are not presently recommended or supported. - - -.. rubric:: Footnotes - -.. [#venv] By using this virtualized Python environment (`virtualenv - `_) we don't pollute the main - OS space with packages from PyPI! From f55ef8e286b52165717b3879f33d3cd3596d193f Mon Sep 17 00:00:00 2001 From: Marius Gedminas Date: Wed, 18 May 2016 11:03:18 +0300 Subject: [PATCH 128/396] Renewal hooks mean this note is outdated --- docs/using.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/using.rst b/docs/using.rst index 12ea3ea11..12f6c7375 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -337,8 +337,8 @@ will cause nasty errors served through the browsers! .. note:: All files are PEM-encoded (as the filename suffix suggests). If you need other format, such as DER or PFX, then you - could convert using ``openssl``, but this means you will not - benefit from automatic renewal_! + could convert using ``openssl``. You can automate that with + `--renew-hook` if you're using automatic renewal_. .. _config-file: From 279cb352568a600ea79df47065be3967890ee109 Mon Sep 17 00:00:00 2001 From: Marius Gedminas Date: Wed, 18 May 2016 11:05:23 +0300 Subject: [PATCH 129/396] Oops, ReST syntax is weird --- docs/using.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/using.rst b/docs/using.rst index 12f6c7375..b10532259 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -338,7 +338,7 @@ will cause nasty errors served through the browsers! .. note:: All files are PEM-encoded (as the filename suffix suggests). If you need other format, such as DER or PFX, then you could convert using ``openssl``. You can automate that with - `--renew-hook` if you're using automatic renewal_. + ``--renew-hook`` if you're using automatic renewal_. .. _config-file: From 321a806b9186572a9e9d735693bf9cd7c97bced3 Mon Sep 17 00:00:00 2001 From: Marius Gedminas Date: Wed, 18 May 2016 11:57:50 +0300 Subject: [PATCH 130/396] Hook validation: skip leading spaces/newlines Improves the situation with #3020 a bit. Does nothing about other valid shell commands that the current validation would reject: - shell builtins like --post-hook 'if [ -x /my/script ]; then /my/script; fi' - variable assignments like --post-hook 'ENV_VAR=value command' - comments - redirections like --post-hook ' Date: Wed, 18 May 2016 09:56:34 -0700 Subject: [PATCH 131/396] Factor loading cert/req into its own function --- certbot/crypto_util.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/certbot/crypto_util.py b/certbot/crypto_util.py index 3f2267af2..41e675471 100644 --- a/certbot/crypto_util.py +++ b/certbot/crypto_util.py @@ -228,15 +228,20 @@ def pyopenssl_load_certificate(data): str(error) for error in openssl_errors))) -def _get_sans_from_cert_or_req(cert_or_req_str, load_func, - typ=OpenSSL.crypto.FILETYPE_PEM): +def _load_cert_or_req(cert_or_req_str, load_func, + typ=OpenSSL.crypto.FILETYPE_PEM): try: - cert_or_req = load_func(typ, cert_or_req_str) + return load_func(typ, cert_or_req_str) except OpenSSL.crypto.Error as error: logger.exception(error) raise + + +def _get_sans_from_cert_or_req(cert_or_req_str, load_func, + typ=OpenSSL.crypto.FILETYPE_PEM): # pylint: disable=protected-access - return acme_crypto_util._pyopenssl_cert_or_req_san(cert_or_req) + return acme_crypto_util._pyopenssl_cert_or_req_san(_load_cert_or_req( + cert_or_req_str, load_func, typ)) def get_sans_from_cert(cert, typ=OpenSSL.crypto.FILETYPE_PEM): From 8e17d7498de484857daead51b56f50b186341233 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 18 May 2016 10:14:15 -0700 Subject: [PATCH 132/396] Add get_names_from_csr --- certbot/crypto_util.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/certbot/crypto_util.py b/certbot/crypto_util.py index 41e675471..b273cf59f 100644 --- a/certbot/crypto_util.py +++ b/certbot/crypto_util.py @@ -272,6 +272,27 @@ def get_sans_from_csr(csr, typ=OpenSSL.crypto.FILETYPE_PEM): csr, OpenSSL.crypto.load_certificate_request, typ) +def get_names_from_csr(csr, typ=OpenSSL.crypto.FILETYPE_PEM): + """Get a list of domains from a CSR, including the CN if it is set. + + :param str csr: CSR (encoded). + :param typ: `OpenSSL.crypto.FILETYPE_PEM` or `OpenSSL.crypto.FILETYPE_ASN1` + + :returns: A list of domain names. + :rtype: list + + """ + loaded_csr = _load_cert_or_req( + csr, OpenSSL.crypto.load_certificate_request, typ) + common_name = loaded_csr.get_subject().CN + + # Use a set to avoid duplication with CN and Subject Alt Names + domains = set() if common_name is None else set((common_name,)) + # pylint: disable=protected-access + domains.update(acme_crypto_util._pyopenssl_cert_or_req_san(loaded_csr)) + return list(domains) + + def dump_pyopenssl_chain(chain, filetype=OpenSSL.crypto.FILETYPE_PEM): """Dump certificate chain into a bundle. From c4fc7b30e397b187dc20d45d20cc6254a359826b Mon Sep 17 00:00:00 2001 From: Noah Swartz Date: Wed, 18 May 2016 13:44:29 -0700 Subject: [PATCH 133/396] change github URL --- letsencrypt-auto-source/letsencrypt-auto | 11 +++++++++-- letsencrypt-auto-source/pieces/fetch.py | 2 +- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/letsencrypt-auto-source/letsencrypt-auto b/letsencrypt-auto-source/letsencrypt-auto index bbb2cda54..83e915e26 100755 --- a/letsencrypt-auto-source/letsencrypt-auto +++ b/letsencrypt-auto-source/letsencrypt-auto @@ -452,6 +452,11 @@ BootstrapMac() { fi } +BootstrapSmartOS() { + pkgin update + pkgin -y install 'gcc49' 'py27-augeas' 'py27-virtualenv' +} + # Install required OS packages: Bootstrap() { @@ -484,8 +489,10 @@ Bootstrap() { ExperimentalBootstrap "FreeBSD" BootstrapFreeBsd elif uname | grep -iq Darwin ; then ExperimentalBootstrap "Mac OS X" BootstrapMac - elif grep -iq "Amazon Linux" /etc/issue ; then + elif [ -f /etc/issue ] && grep -iq "Amazon Linux" /etc/issue ; then ExperimentalBootstrap "Amazon Linux" BootstrapRpmCommon + elif [ -f /etc/product ] && grep -q "Joyent Instance" /etc/product ; then + ExperimentalBootstrap "Joyent SmartOS Zone" BootstrapSmartOS else echo "Sorry, I don't know how to bootstrap Certbot on your operating system!" echo @@ -1017,7 +1024,7 @@ def verified_new_le_auto(get, tag, temp_dir): """ le_auto_dir = environ.get( 'LE_AUTO_DIR_TEMPLATE', - 'https://raw.githubusercontent.com/letsencrypt/letsencrypt/%s/' + 'https://raw.githubusercontent.com/certbot/certbot/%s/' 'letsencrypt-auto-source/') % tag write(get(le_auto_dir + 'letsencrypt-auto'), temp_dir, 'letsencrypt-auto') write(get(le_auto_dir + 'letsencrypt-auto.sig'), temp_dir, 'letsencrypt-auto.sig') diff --git a/letsencrypt-auto-source/pieces/fetch.py b/letsencrypt-auto-source/pieces/fetch.py index 38f4aa255..ca3e94b80 100644 --- a/letsencrypt-auto-source/pieces/fetch.py +++ b/letsencrypt-auto-source/pieces/fetch.py @@ -87,7 +87,7 @@ def verified_new_le_auto(get, tag, temp_dir): """ le_auto_dir = environ.get( 'LE_AUTO_DIR_TEMPLATE', - 'https://raw.githubusercontent.com/letsencrypt/letsencrypt/%s/' + 'https://raw.githubusercontent.com/certbot/certbot/%s/' 'letsencrypt-auto-source/') % tag write(get(le_auto_dir + 'letsencrypt-auto'), temp_dir, 'letsencrypt-auto') write(get(le_auto_dir + 'letsencrypt-auto.sig'), temp_dir, 'letsencrypt-auto.sig') From 77e4be933cbfffaadda97d45e2dfb17672de56b5 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 18 May 2016 13:59:17 -0700 Subject: [PATCH 134/396] Simplify get_names_from_csr --- certbot/crypto_util.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/certbot/crypto_util.py b/certbot/crypto_util.py index b273cf59f..1f87dc816 100644 --- a/certbot/crypto_util.py +++ b/certbot/crypto_util.py @@ -284,10 +284,8 @@ def get_names_from_csr(csr, typ=OpenSSL.crypto.FILETYPE_PEM): """ loaded_csr = _load_cert_or_req( csr, OpenSSL.crypto.load_certificate_request, typ) - common_name = loaded_csr.get_subject().CN - # Use a set to avoid duplication with CN and Subject Alt Names - domains = set() if common_name is None else set((common_name,)) + domains = set(d for d in (loaded_csr.get_subject().CN,) if d is not None) # pylint: disable=protected-access domains.update(acme_crypto_util._pyopenssl_cert_or_req_san(loaded_csr)) return list(domains) From 94549219c593dfc687b01f65ee89dc2287dae06e Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 18 May 2016 14:06:32 -0700 Subject: [PATCH 135/396] Add get_names_from_csr tests --- certbot/tests/crypto_util_test.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/certbot/tests/crypto_util_test.py b/certbot/tests/crypto_util_test.py index ff8d8142e..eade4861f 100644 --- a/certbot/tests/crypto_util_test.py +++ b/certbot/tests/crypto_util_test.py @@ -234,6 +234,36 @@ class GetSANsFromCSRTest(unittest.TestCase): [], self._call(test_util.load_vector('csr-nosans.pem'))) +class GetNamesFromCSRTest(unittest.TestCase): + """Tests for certbot.crypto_util.get_names_from_csr.""" + @classmethod + def _call(cls, *args, **kwargs): + from certbot.crypto_util import get_names_from_csr + return get_names_from_csr(*args, **kwargs) + + def test_extract_one_san(self): + self.assertEqual(['example.com'], self._call( + test_util.load_vector('csr.pem'))) + + def test_extract_two_sans(self): + self.assertEqual(set(('example.com', 'www.example.com',)), set( + self._call(test_util.load_vector('csr-san.pem')))) + + def test_extract_six_sans(self): + self.assertEqual( + set(self._call(test_util.load_vector('csr-6sans.pem'))), + set(("example.com", "example.org", "example.net", + "example.info", "subdomain.example.com", + "other.subdomain.example.com",))) + + def test_parse_non_csr(self): + self.assertRaises(OpenSSL.crypto.Error, self._call, "hello there") + + def test_parse_no_sans(self): + self.assertEqual(["example.org"], + self._call(test_util.load_vector('csr-nosans.pem'))) + + class CertLoaderTest(unittest.TestCase): """Tests for certbot.crypto_util.pyopenssl_load_certificate""" From 01ebab26bfa0eee521e8767411419007efa50814 Mon Sep 17 00:00:00 2001 From: Noah Swartz Date: Wed, 18 May 2016 14:21:57 -0700 Subject: [PATCH 136/396] update pypi for auto --- letsencrypt-auto-source/letsencrypt-auto | 11 +++++++++-- letsencrypt-auto-source/pieces/fetch.py | 2 +- letsencrypt-auto-source/tests/auto_test.py | 2 +- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/letsencrypt-auto-source/letsencrypt-auto b/letsencrypt-auto-source/letsencrypt-auto index bbb2cda54..dd7dc06ec 100755 --- a/letsencrypt-auto-source/letsencrypt-auto +++ b/letsencrypt-auto-source/letsencrypt-auto @@ -452,6 +452,11 @@ BootstrapMac() { fi } +BootstrapSmartOS() { + pkgin update + pkgin -y install 'gcc49' 'py27-augeas' 'py27-virtualenv' +} + # Install required OS packages: Bootstrap() { @@ -484,8 +489,10 @@ Bootstrap() { ExperimentalBootstrap "FreeBSD" BootstrapFreeBsd elif uname | grep -iq Darwin ; then ExperimentalBootstrap "Mac OS X" BootstrapMac - elif grep -iq "Amazon Linux" /etc/issue ; then + elif [ -f /etc/issue ] && grep -iq "Amazon Linux" /etc/issue ; then ExperimentalBootstrap "Amazon Linux" BootstrapRpmCommon + elif [ -f /etc/product ] && grep -q "Joyent Instance" /etc/product ; then + ExperimentalBootstrap "Joyent SmartOS Zone" BootstrapSmartOS else echo "Sorry, I don't know how to bootstrap Certbot on your operating system!" echo @@ -998,7 +1005,7 @@ def latest_stable_version(get): """Return the latest stable release of letsencrypt.""" metadata = loads(get( environ.get('LE_AUTO_JSON_URL', - 'https://pypi.python.org/pypi/letsencrypt/json'))) + 'https://pypi.python.org/pypi/certbot/json'))) # metadata['info']['version'] actually returns the latest of any kind of # release release, contrary to https://wiki.python.org/moin/PyPIJSON. # The regex is a sufficient regex for picking out prereleases for most diff --git a/letsencrypt-auto-source/pieces/fetch.py b/letsencrypt-auto-source/pieces/fetch.py index 38f4aa255..4a2287fff 100644 --- a/letsencrypt-auto-source/pieces/fetch.py +++ b/letsencrypt-auto-source/pieces/fetch.py @@ -68,7 +68,7 @@ def latest_stable_version(get): """Return the latest stable release of letsencrypt.""" metadata = loads(get( environ.get('LE_AUTO_JSON_URL', - 'https://pypi.python.org/pypi/letsencrypt/json'))) + 'https://pypi.python.org/pypi/certbot/json'))) # metadata['info']['version'] actually returns the latest of any kind of # release release, contrary to https://wiki.python.org/moin/PyPIJSON. # The regex is a sufficient regex for picking out prereleases for most diff --git a/letsencrypt-auto-source/tests/auto_test.py b/letsencrypt-auto-source/tests/auto_test.py index 3b7e8731b..2c733f858 100644 --- a/letsencrypt-auto-source/tests/auto_test.py +++ b/letsencrypt-auto-source/tests/auto_test.py @@ -183,7 +183,7 @@ def run_le_auto(venv_dir, base_url, **kwargs): d = dict(XDG_DATA_HOME=venv_dir, # URL to PyPI-style JSON that tell us the latest released version # of LE: - LE_AUTO_JSON_URL=base_url + 'letsencrypt/json', + LE_AUTO_JSON_URL=base_url + 'certbot/json', # URL to dir containing letsencrypt-auto and letsencrypt-auto.sig: LE_AUTO_DIR_TEMPLATE=base_url + '%s/', # The public key corresponding to signing.key: From df9174b81f18328fd5b85a57cf21f4247268cb04 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Wed, 18 May 2016 14:36:07 -0700 Subject: [PATCH 137/396] Fix whitespace --- certbot/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certbot/cli.py b/certbot/cli.py index 41d31fa35..4585446bd 100644 --- a/certbot/cli.py +++ b/certbot/cli.py @@ -760,7 +760,7 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): "renew", "--post-hook", help="Command to be run in a shell after attempting to obtain/renew " " certificates. Can be used to deploy renewed certificates, or to restart" - " any servers that were stopped by --pre-hook. This is only run if " + " any servers that were stopped by --pre-hook. This is only run if" " an attempt was made to obtain/renew a certificate.") helpful.add( "renew", "--renew-hook", From 70912be5a9a1000e235ff74d9935cfc2aef9b3c9 Mon Sep 17 00:00:00 2001 From: Seth Schoen Date: Wed, 18 May 2016 14:57:31 -0700 Subject: [PATCH 138/396] Associate --update-registration with register help topic --- certbot/cli.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/certbot/cli.py b/certbot/cli.py index d42d77412..7f3779e5b 100644 --- a/certbot/cli.py +++ b/certbot/cli.py @@ -88,7 +88,8 @@ More detailed help: the available topics are: all, automation, paths, security, testing, or any of the subcommands or - plugins (certonly, install, nginx, apache, standalone, webroot, etc) + plugins (certonly, install, register, nginx, apache, standalone, webroot, + etc.) """ @@ -615,7 +616,7 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): "affect you, and will be effective 14 days after posting an " "update to the web site.") helpful.add( - None, "--update-registration", action="store_true", + "register", "--update-registration", action="store_true", help="With the register verb, indicates that details associated " "with an existing registration, such as the e-mail address, " "should be updated, rather than registering a new account.") From 55755d818af57809cd23ec6ccff855ae8a30c50a Mon Sep 17 00:00:00 2001 From: Noah Swartz Date: Wed, 18 May 2016 15:42:55 -0700 Subject: [PATCH 139/396] update secret pypi? --- letsencrypt-auto-source/tests/auto_test.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/letsencrypt-auto-source/tests/auto_test.py b/letsencrypt-auto-source/tests/auto_test.py index 2c733f858..8018bab0f 100644 --- a/letsencrypt-auto-source/tests/auto_test.py +++ b/letsencrypt-auto-source/tests/auto_test.py @@ -301,8 +301,8 @@ class AutoTests(TestCase): with ephemeral_dir() as venv_dir: # Serve an unrelated hash signed with the good key (easier than # making a bad key, and a mismatch is a mismatch): - resources = {'': 'letsencrypt/', - 'letsencrypt/json': dumps({'releases': {'99.9.9': None}}), + resources = {'': 'certbot/', + 'certbot/json': dumps({'releases': {'99.9.9': None}}), 'v99.9.9/letsencrypt-auto': build_le_auto(version='99.9.9'), 'v99.9.9/letsencrypt-auto.sig': signed('something else')} with serving(resources) as base_url: From 2ba5ce9217433cb913edf408477c96a710a91e22 Mon Sep 17 00:00:00 2001 From: Seth Schoen Date: Wed, 18 May 2016 15:55:25 -0700 Subject: [PATCH 140/396] Mention register subcommand in main help --- certbot/cli.py | 1 + 1 file changed, 1 insertion(+) diff --git a/certbot/cli.py b/certbot/cli.py index 7f3779e5b..f8be642d8 100644 --- a/certbot/cli.py +++ b/certbot/cli.py @@ -63,6 +63,7 @@ cert. Major SUBCOMMANDS are: install Install a previously obtained cert in a server renew Renew previously obtained certs that are near expiry revoke Revoke a previously obtained certificate + register Perform tasks related to registering with the CA rollback Rollback server configuration changes made during install config_changes Show changes made to server config during installation plugins Display information about installed plugins From e385274cca6814b3e910e82a09574fae349d68e2 Mon Sep 17 00:00:00 2001 From: Telepenin Nikolay Date: Thu, 19 May 2016 02:35:17 +0300 Subject: [PATCH 141/396] Error/Warning with build docker container from Dockerfile (#3004) When I try to build container I see in logs ``` debconf: unable to initialize frontend: Dialog debconf: (TERM is not set, so the dialog frontend is not usable.) debconf: falling back to frontend: Readline debconf: unable to initialize frontend: Readline debconf: (This frontend requires a controlling tty.) debconf: falling back to frontend: Teletype ``` `DEBIAN_FRONTEND=noninteractive` fixed this warning --- Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Dockerfile b/Dockerfile index 3e4c9430e..d42b632d4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -21,6 +21,7 @@ WORKDIR /opt/certbot # If doesn't exist, it is created along with all missing # directories in its path. +ENV DEBIAN_FRONTEND=noninteractive COPY letsencrypt-auto-source/letsencrypt-auto /opt/certbot/src/letsencrypt-auto-source/letsencrypt-auto RUN /opt/certbot/src/letsencrypt-auto-source/letsencrypt-auto --os-packages-only && \ From e8e009cc854246e9b059783f77506328c417191c Mon Sep 17 00:00:00 2001 From: Noah Swartz Date: Wed, 18 May 2016 17:00:42 -0700 Subject: [PATCH 142/396] Revert "update secret pypi?" This reverts commit 55755d818af57809cd23ec6ccff855ae8a30c50a. --- letsencrypt-auto-source/tests/auto_test.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/letsencrypt-auto-source/tests/auto_test.py b/letsencrypt-auto-source/tests/auto_test.py index 8018bab0f..2c733f858 100644 --- a/letsencrypt-auto-source/tests/auto_test.py +++ b/letsencrypt-auto-source/tests/auto_test.py @@ -301,8 +301,8 @@ class AutoTests(TestCase): with ephemeral_dir() as venv_dir: # Serve an unrelated hash signed with the good key (easier than # making a bad key, and a mismatch is a mismatch): - resources = {'': 'certbot/', - 'certbot/json': dumps({'releases': {'99.9.9': None}}), + resources = {'': 'letsencrypt/', + 'letsencrypt/json': dumps({'releases': {'99.9.9': None}}), 'v99.9.9/letsencrypt-auto': build_le_auto(version='99.9.9'), 'v99.9.9/letsencrypt-auto.sig': signed('something else')} with serving(resources) as base_url: From 574d20ecc46c989d4b8b7e808c668302f6e4d4ff Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Thu, 19 May 2016 09:28:26 -0700 Subject: [PATCH 143/396] Record enhancements applied to vhosts --- certbot-apache/certbot_apache/configurator.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/certbot-apache/certbot_apache/configurator.py b/certbot-apache/certbot_apache/configurator.py index 12125d522..bf9a388ee 100644 --- a/certbot-apache/certbot_apache/configurator.py +++ b/certbot-apache/certbot_apache/configurator.py @@ -124,6 +124,8 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): self.assoc = dict() # Outstanding challenges self._chall_out = set() + # Maps enhancements to vhosts we've enabled the enhancement for + self._enhanced_vhosts = defaultdict(set) # These will be set in the prepare function self.parser = None @@ -1058,9 +1060,6 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): :param unused_options: Not currently used :type unused_options: Not Available - :returns: Success, general_vhost (HTTP vhost) - :rtype: (bool, :class:`~certbot_apache.obj.VirtualHost`) - :raises .errors.PluginError: If no viable HTTP host can be created or used for the redirect. @@ -1083,6 +1082,10 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): "redirection") self._create_redirect_vhost(ssl_vhost) else: + if general_vh in self._enhanced_vhosts["redirect"]: + logger.debug("Already enabled redirect for this vhost") + return + # Check if Certbot redirection already exists self._verify_no_certbot_redirect(general_vh) @@ -1118,6 +1121,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): (general_vh.filep, ssl_vhost.filep)) self.save() + self._enhanced_vhosts["redirect"].add(general_vh) logger.info("Redirecting vhost in %s to ssl vhost in %s", general_vh.filep, ssl_vhost.filep) @@ -1206,6 +1210,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): # Make a new vhost data structure and add it to the lists new_vhost = self._create_vhost(parser.get_aug_path(redirect_filepath)) self.vhosts.append(new_vhost) + self._enhanced_vhosts["redirect"].add(new_vhost) # Finally create documentation for the change self.save_notes += ("Created a port 80 vhost, %s, for redirection to " From 66a13999208ed89a3664a009b589499338287bd0 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Thu, 19 May 2016 09:40:17 -0700 Subject: [PATCH 144/396] Add tests for multidomain vhost redirects --- .../certbot_apache/tests/configurator_test.py | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/certbot-apache/certbot_apache/tests/configurator_test.py b/certbot-apache/certbot_apache/tests/configurator_test.py index f2f78c8f9..978d9f5c7 100644 --- a/certbot-apache/certbot_apache/tests/configurator_test.py +++ b/certbot-apache/certbot_apache/tests/configurator_test.py @@ -938,15 +938,31 @@ class MultipleVhostsTest(util.ApacheTest): self.assertRaises( errors.PluginError, self.config._enable_redirect, ssl_vh, "") - def test_redirect_twice(self): + def test_redirect_two_domains_one_vhost(self): # Skip the enable mod self.config.parser.modules.add("rewrite_module") self.config.get_version = mock.Mock(return_value=(2, 3, 9)) - self.config.enhance("encryption-example.demo", "redirect") + self.config.enhance("red.blue.purple.com", "redirect") + verify_no_redirect = ("certbot_apache.configurator." + "ApacheConfigurator._verify_no_certbot_redirect") + with mock.patch(verify_no_redirect) as mock_verify: + self.config.enhance("green.blue.purple.com", "redirect") + self.assertFalse(mock_verify.called) + + def test_redirect_from_previous_run(self): + # Skip the enable mod + self.config.parser.modules.add("rewrite_module") + self.config.get_version = mock.Mock(return_value=(2, 3, 9)) + + self.config.enhance("red.blue.purple.com", "redirect") + # Clear state about enabling redirect on this run + # pylint: disable=protected-access + self.config._enhanced_vhosts["redirect"].clear() + self.assertRaises( errors.PluginEnhancementAlreadyPresent, - self.config.enhance, "encryption-example.demo", "redirect") + self.config.enhance, "green.blue.purple.com", "redirect") def test_create_own_redirect(self): self.config.parser.modules.add("rewrite_module") From e7374811294be55b45d8de30a36883f836da6590 Mon Sep 17 00:00:00 2001 From: sagi Date: Thu, 19 May 2016 18:20:27 +0000 Subject: [PATCH 145/396] WIP --- certbot/client.py | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/certbot/client.py b/certbot/client.py index dd38e47b0..6dd0420eb 100644 --- a/certbot/client.py +++ b/certbot/client.py @@ -292,6 +292,7 @@ class Client(object): key.pem, crypto_util.dump_pyopenssl_chain(chain), configuration.RenewerConfiguration(self.config.namespace)) + def save_certificate(self, certr, chain_cert, cert_path, chain_path, fullchain_path): """Saves the certificate received from the ACME server. @@ -318,12 +319,15 @@ class Client(object): cert_pem = OpenSSL.crypto.dump_certificate( OpenSSL.crypto.FILETYPE_PEM, certr.body.wrapped) - + """ if cli.set_by_cli('cert_path'): cert_file = le_util.safe_open(cert_path, chmod=0o644) act_cert_path = cert_path else: cert_file, act_cert_path = le_util.unique_file(cert_path, 0o644) + """ + cert_file, act_cert_path = _open_pem_file('cert_path', cert_path) + #import ipdb; ipdb.set_trace try: cert_file.write(cert_pem) @@ -331,7 +335,14 @@ class Client(object): cert_file.close() logger.info("Server issued certificate; certificate written to %s", act_cert_path) - + + if cli.set_by_cli('chain_path'): + #import ipdb; ipdb.set_trace() + pass + if cli.set_by_cli('fullchain_path'): + #import ipdb; ipdb.set_trace() + pass + cert_chain_abspath = None fullchain_abspath = None if chain_cert: @@ -569,6 +580,11 @@ def view_config_changes(config, num=None): rev.recovery_routine() rev.view_config_changes(num) +def _open_pem_file(cli_arg_path, pem_path): + if cli.set_by_cli(cli_arg_path): + return le_util.safe_open(pem_path, chmod=0o644), pem_path + else: + return le_util.unique_file(pem_path, 0o644) def _save_chain(chain_pem, chain_path): """Saves chain_pem at a unique path based on chain_path. From 409640fb87a89487e46be0427c072a05074958a0 Mon Sep 17 00:00:00 2001 From: Noah Swartz Date: Thu, 19 May 2016 12:05:42 -0700 Subject: [PATCH 146/396] le to cb for test package --- letsencrypt-auto-source/tests/auto_test.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/letsencrypt-auto-source/tests/auto_test.py b/letsencrypt-auto-source/tests/auto_test.py index 2c733f858..357e8302c 100644 --- a/letsencrypt-auto-source/tests/auto_test.py +++ b/letsencrypt-auto-source/tests/auto_test.py @@ -258,9 +258,9 @@ class AutoTests(TestCase): with ephemeral_dir() as venv_dir: # This serves a PyPI page with a higher version, a GitHub-alike # with a corresponding le-auto script, and a matching signature. - resources = {'letsencrypt/json': dumps({'releases': {'99.9.9': None}}), - 'v99.9.9/letsencrypt-auto': NEW_LE_AUTO, - 'v99.9.9/letsencrypt-auto.sig': NEW_LE_AUTO_SIG} + resources = {'certbot/json': dumps({'releases': {'99.9.9': None}}), + 'v99.9.9/certbot-auto': NEW_LE_AUTO, + 'v99.9.9/certbot-auto.sig': NEW_LE_AUTO_SIG} with serving(resources) as base_url: run_letsencrypt_auto = partial( run_le_auto, @@ -301,10 +301,10 @@ class AutoTests(TestCase): with ephemeral_dir() as venv_dir: # Serve an unrelated hash signed with the good key (easier than # making a bad key, and a mismatch is a mismatch): - resources = {'': 'letsencrypt/', - 'letsencrypt/json': dumps({'releases': {'99.9.9': None}}), - 'v99.9.9/letsencrypt-auto': build_le_auto(version='99.9.9'), - 'v99.9.9/letsencrypt-auto.sig': signed('something else')} + resources = {'': 'certbot/', + 'certbot/json': dumps({'releases': {'99.9.9': None}}), + 'v99.9.9/certbot-auto': build_le_auto(version='99.9.9'), + 'v99.9.9/certbot-auto.sig': signed('something else')} with serving(resources) as base_url: copy(LE_AUTO_PATH, venv_dir) try: @@ -320,8 +320,8 @@ class AutoTests(TestCase): def test_pip_failure(self): """Make sure pip stops us if there is a hash mismatch.""" with ephemeral_dir() as venv_dir: - resources = {'': 'letsencrypt/', - 'letsencrypt/json': dumps({'releases': {'99.9.9': None}})} + resources = {'': 'certbot/', + 'certbot/json': dumps({'releases': {'99.9.9': None}})} with serving(resources) as base_url: # Build a le-auto script embedding a bad requirements file: install_le_auto( From fde151848d4b1a29ada511db77308979ba247989 Mon Sep 17 00:00:00 2001 From: sagi Date: Thu, 19 May 2016 19:11:25 +0000 Subject: [PATCH 147/396] Use set_by_cli for fullchain_path and chain_path --- certbot/client.py | 40 ++++++++++++++++------------------------ certbot/le_util.py | 3 ++- 2 files changed, 18 insertions(+), 25 deletions(-) diff --git a/certbot/client.py b/certbot/client.py index 6dd0420eb..3475312f0 100644 --- a/certbot/client.py +++ b/certbot/client.py @@ -319,15 +319,8 @@ class Client(object): cert_pem = OpenSSL.crypto.dump_certificate( OpenSSL.crypto.FILETYPE_PEM, certr.body.wrapped) - """ - if cli.set_by_cli('cert_path'): - cert_file = le_util.safe_open(cert_path, chmod=0o644) - act_cert_path = cert_path - else: - cert_file, act_cert_path = le_util.unique_file(cert_path, 0o644) - """ + cert_file, act_cert_path = _open_pem_file('cert_path', cert_path) - #import ipdb; ipdb.set_trace try: cert_file.write(cert_pem) @@ -335,21 +328,20 @@ class Client(object): cert_file.close() logger.info("Server issued certificate; certificate written to %s", act_cert_path) - - if cli.set_by_cli('chain_path'): - #import ipdb; ipdb.set_trace() - pass - if cli.set_by_cli('fullchain_path'): - #import ipdb; ipdb.set_trace() - pass - + cert_chain_abspath = None fullchain_abspath = None if chain_cert: chain_pem = crypto_util.dump_pyopenssl_chain(chain_cert) - cert_chain_abspath = _save_chain(chain_pem, chain_path) + + chain_file, act_chain_path =\ + _open_pem_file('chain_path', chain_path) + fullchain_file, act_fullchain_path =\ + _open_pem_file('fullchain_path', fullchain_path) + + cert_chain_abspath = _save_chain(chain_pem, chain_file) fullchain_abspath = _save_chain(cert_pem + chain_pem, - fullchain_path) + fullchain_file) return os.path.abspath(act_cert_path), cert_chain_abspath, fullchain_abspath @@ -582,27 +574,27 @@ def view_config_changes(config, num=None): def _open_pem_file(cli_arg_path, pem_path): if cli.set_by_cli(cli_arg_path): - return le_util.safe_open(pem_path, chmod=0o644), pem_path + return le_util.safe_open(pem_path, chmod=0o644),\ + os.path.abspath(pem_path) else: return le_util.unique_file(pem_path, 0o644) -def _save_chain(chain_pem, chain_path): +def _save_chain(chain_pem, chain_file): """Saves chain_pem at a unique path based on chain_path. :param str chain_pem: certificate chain in PEM format - :param str chain_path: candidate path for the cert chain + :param str chain_file: chain file object :returns: absolute path to saved cert chain :rtype: str """ - chain_file, act_chain_path = le_util.unique_file(chain_path, 0o644) try: chain_file.write(chain_pem) finally: chain_file.close() - logger.info("Cert chain written to %s", act_chain_path) + logger.info("Cert chain written to %s", chain_file.name) # This expects a valid chain file - return os.path.abspath(act_chain_path) + return os.path.abspath(chain_file.name) diff --git a/certbot/le_util.py b/certbot/le_util.py index f5148b949..fe2577a4c 100644 --- a/certbot/le_util.py +++ b/certbot/le_util.py @@ -151,7 +151,8 @@ def _unique_file(path, filename_pat, count, mode): while True: current_path = os.path.join(path, filename_pat(count)) try: - return safe_open(current_path, chmod=mode), current_path + return safe_open(current_path, chmod=mode),\ + os.path.abspath(current_path) except OSError as err: # "File exists," is okay, try a different name. if err.errno != errno.EEXIST: From 3aed4fc59d355df158b08f11f96df084b6a53e9b Mon Sep 17 00:00:00 2001 From: Christopher Brown Date: Thu, 19 May 2016 14:19:13 -0500 Subject: [PATCH 148/396] Typo: too many self's The extra self will push along the arguments, resulting in the accurate but not very helpful error message: "AttributeError: 'JWKRSA' object has no attribute 'kty'" --- acme/acme/challenges.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/acme/acme/challenges.py b/acme/acme/challenges.py index 280bc8308..c436cc631 100644 --- a/acme/acme/challenges.py +++ b/acme/acme/challenges.py @@ -500,7 +500,7 @@ class DNS(_TokenChallenge): """ return DNSResponse(validation=self.gen_validation( - self, account_key, **kwargs)) + account_key, **kwargs)) def validation_domain_name(self, name): """Domain name for TXT validation record. From 0bb8b0bcd5231c896ee9449bac22d30204381dac Mon Sep 17 00:00:00 2001 From: Noah Swartz Date: Thu, 19 May 2016 12:27:17 -0700 Subject: [PATCH 149/396] change invocation --- letsencrypt-auto-source/tests/auto_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/letsencrypt-auto-source/tests/auto_test.py b/letsencrypt-auto-source/tests/auto_test.py index 357e8302c..6fccdb56e 100644 --- a/letsencrypt-auto-source/tests/auto_test.py +++ b/letsencrypt-auto-source/tests/auto_test.py @@ -199,7 +199,7 @@ iQIDAQAB **kwargs) env.update(d) return out_and_err( - join(venv_dir, 'letsencrypt-auto') + ' --version', + join(venv_dir, 'certbot-auto') + ' --version', shell=True, env=env) From e1eb3eff164610b7359582478d02c3fbf4b8c6b9 Mon Sep 17 00:00:00 2001 From: sagi Date: Thu, 19 May 2016 19:27:18 +0000 Subject: [PATCH 150/396] Improve code reuse --- certbot/client.py | 26 +++++++++----------------- 1 file changed, 9 insertions(+), 17 deletions(-) diff --git a/certbot/client.py b/certbot/client.py index 3475312f0..ee1ab8bb8 100644 --- a/certbot/client.py +++ b/certbot/client.py @@ -320,30 +320,27 @@ class Client(object): cert_pem = OpenSSL.crypto.dump_certificate( OpenSSL.crypto.FILETYPE_PEM, certr.body.wrapped) - cert_file, act_cert_path = _open_pem_file('cert_path', cert_path) + cert_file, abs_cert_path = _open_pem_file('cert_path', cert_path) try: cert_file.write(cert_pem) finally: cert_file.close() logger.info("Server issued certificate; certificate written to %s", - act_cert_path) + abs_cert_path) - cert_chain_abspath = None - fullchain_abspath = None if chain_cert: chain_pem = crypto_util.dump_pyopenssl_chain(chain_cert) - chain_file, act_chain_path =\ + chain_file, abs_chain_path =\ _open_pem_file('chain_path', chain_path) - fullchain_file, act_fullchain_path =\ + fullchain_file, abs_fullchain_path =\ _open_pem_file('fullchain_path', fullchain_path) - cert_chain_abspath = _save_chain(chain_pem, chain_file) - fullchain_abspath = _save_chain(cert_pem + chain_pem, - fullchain_file) + _save_chain(chain_pem, chain_file) + _save_chain(cert_pem + chain_pem, fullchain_file - return os.path.abspath(act_cert_path), cert_chain_abspath, fullchain_abspath + return abs_cert_path, abs_chain_path, abs_fullchain_path def deploy_certificate(self, domains, privkey_path, cert_path, chain_path, fullchain_path): @@ -577,7 +574,8 @@ def _open_pem_file(cli_arg_path, pem_path): return le_util.safe_open(pem_path, chmod=0o644),\ os.path.abspath(pem_path) else: - return le_util.unique_file(pem_path, 0o644) + uniq = le_util.unique_file(pem_path, 0o644) + return uniq[0], os.path.abspath(uniq) def _save_chain(chain_pem, chain_file): """Saves chain_pem at a unique path based on chain_path. @@ -585,9 +583,6 @@ def _save_chain(chain_pem, chain_file): :param str chain_pem: certificate chain in PEM format :param str chain_file: chain file object - :returns: absolute path to saved cert chain - :rtype: str - """ try: chain_file.write(chain_pem) @@ -595,6 +590,3 @@ def _save_chain(chain_pem, chain_file): chain_file.close() logger.info("Cert chain written to %s", chain_file.name) - - # This expects a valid chain file - return os.path.abspath(chain_file.name) From 501c19ef2a254e44b768eb4cd57e5832f5afb792 Mon Sep 17 00:00:00 2001 From: sagi Date: Thu, 19 May 2016 19:33:04 +0000 Subject: [PATCH 151/396] Syntax --- certbot/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certbot/client.py b/certbot/client.py index ee1ab8bb8..8964686e6 100644 --- a/certbot/client.py +++ b/certbot/client.py @@ -338,7 +338,7 @@ class Client(object): _open_pem_file('fullchain_path', fullchain_path) _save_chain(chain_pem, chain_file) - _save_chain(cert_pem + chain_pem, fullchain_file + _save_chain(cert_pem + chain_pem, fullchain_file) return abs_cert_path, abs_chain_path, abs_fullchain_path From 3589b25dc3a7d4c0fb2477b21d6039a327bd1b52 Mon Sep 17 00:00:00 2001 From: sagi Date: Thu, 19 May 2016 19:35:38 +0000 Subject: [PATCH 152/396] Make lint happy --- certbot/client.py | 1 - 1 file changed, 1 deletion(-) diff --git a/certbot/client.py b/certbot/client.py index 8964686e6..4fc662948 100644 --- a/certbot/client.py +++ b/certbot/client.py @@ -292,7 +292,6 @@ class Client(object): key.pem, crypto_util.dump_pyopenssl_chain(chain), configuration.RenewerConfiguration(self.config.namespace)) - def save_certificate(self, certr, chain_cert, cert_path, chain_path, fullchain_path): """Saves the certificate received from the ACME server. From ad76de2502596a9ed76b9785108964f79882c21b Mon Sep 17 00:00:00 2001 From: Sagi Kedmi Date: Thu, 19 May 2016 16:04:18 -0700 Subject: [PATCH 153/396] OCSP Stapling Enhancement for Apache (#2723) Currently supports only Apache >=2.3.3. letsencrypt --staple-ocsp -d dumpbits.com [no problem to set it on for apache => 2.3.3] To check OCSP Stapling: [~]$ echo QUIT | openssl s_client -connect dumpbits.com:443 -status 2>/dev/null | grep -A 31 'OCSP Resp' OCSP Response Data: OCSP Response Status: successful (0x0) Response Type: Basic OCSP Response Version: 1 (0x0) Responder Id: C = US, O = Let's Encrypt, CN = Let's Encrypt Authority X3 Produced At: Mar 26 17:54:00 2016 GMT Responses: Certificate ID: Hash Algorithm: sha1 Issuer Name Hash: 7EE66AE7729AB3FCF8A220646C16A12D6071085D Issuer Key Hash: A84A6A63047DDDBAE6D139B7A64565EFF3A8ECA1 Serial Number: 032A2108AAA650E6EE2E6B041C03C2612A19 Cert Status: good This Update: Mar 26 17:00:00 2016 GMT Next Update: Apr 2 17:00:00 2016 GMT Signature Algorithm: sha256WithRSAEncryption 64:f2:71:02:6a:97:d9:eb:13:c1:5c:7a:f5:eb:26:89:3b:40: e3:08:82:f7:71:d4:fa:61:4a:8e:4a:7d:e9:53:84:e9:3a:89: 67:66:08:d9:0e:79:65:9a:8d:dc:fb:07:cc:93:4f:eb:4e:3c: cc:7f:cd:fd:db:8f:c3:25:c3:54:87:a9:9c:35:6f:c1:39:31: e0:b1:f6:b1:3d:52:5d:db:bb:69:0f:23:05:fe:33:29:1f:ff: c6:af:17:a5:98:58:50:3a:48:93:5c:09:4b:f3:91:36:48:31: ed:ee:47:4d:66:c3:25:cf:56:b7:f4:48:80:eb:b8:f0:27:b1: 97:18:b4:88:71:c6:55:5d:bb:25:16:48:98:85:8a:12:8d:64: bf:51:df:39:b1:44:91:e1:f2:c6:c3:7d:23:2b:d2:0f:4c:7f: 57:b1:c9:ae:ec:32:b5:6a:87:bd:83:43:f1:f7:3c:8c:11:5c: 9d:a5:12:fa:e6:79:87:45:c6:1d:46:c8:14:1e:8d:d1:de:7a: 0d:e4:53:f2:c9:b6:e5:6e:cb:91:14:bb:04:38:36:4f:71:55: e1:ff:71:c7:a6:31:ed:db:6c:0f:d7:f5:ef:0c:6e:08:6b:e0: 37:cf:ca:a5:67:89:c2:de:8e:36:6d:2f:41:7f:9f:10:c6:de: 4d:b1:2d:09 ====================================== --- certbot-apache/certbot_apache/configurator.py | 69 ++++++++- .../certbot_apache/tests/configurator_test.py | 141 +++++++++++++++--- .../apache2/sites-available/ocsp-ssl.conf | 36 +++++ .../apache2/sites-enabled/ocsp-ssl.conf | 1 + .../debian_apache_2_4/multiple_vhosts/sites | 1 + certbot-apache/certbot_apache/tests/util.py | 8 +- certbot/cli.py | 13 +- certbot/client.py | 7 +- 8 files changed, 246 insertions(+), 30 deletions(-) create mode 100644 certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/multiple_vhosts/apache2/sites-available/ocsp-ssl.conf create mode 120000 certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/multiple_vhosts/apache2/sites-enabled/ocsp-ssl.conf diff --git a/certbot-apache/certbot_apache/configurator.py b/certbot-apache/certbot_apache/configurator.py index 12125d522..cacd54d5b 100644 --- a/certbot-apache/certbot_apache/configurator.py +++ b/certbot-apache/certbot_apache/configurator.py @@ -130,7 +130,8 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): self.version = version self.vhosts = None self._enhance_func = {"redirect": self._enable_redirect, - "ensure-http-header": self._set_http_header} + "ensure-http-header": self._set_http_header, + "staple-ocsp": self._enable_ocsp_stapling} @property def mod_ssl_conf(self): @@ -593,8 +594,8 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): :type addr: :class:`~certbot_apache.obj.Addr` """ - loc = parser.get_aug_path(self.parser.loc["name"]) + loc = parser.get_aug_path(self.parser.loc["name"]) if addr.get_port() == "443": path = self.parser.add_dir_to_ifmodssl( loc, "NameVirtualHost", [str(addr)]) @@ -944,7 +945,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): ###################################################################### def supported_enhancements(self): # pylint: disable=no-self-use """Returns currently supported enhancements.""" - return ["redirect", "ensure-http-header"] + return ["redirect", "ensure-http-header", "staple-ocsp"] def enhance(self, domain, enhancement, options=None): """Enhance configuration. @@ -971,6 +972,68 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): logger.warn("Failed %s for %s", enhancement, domain) raise + def _enable_ocsp_stapling(self, ssl_vhost, unused_options): + """Enables OCSP Stapling + + In OCSP, each client (e.g. browser) would have to query the + OCSP Responder to validate that the site certificate was not revoked. + + Enabling OCSP Stapling, would allow the web-server to query the OCSP + Responder, and staple its response to the offered certificate during + TLS. i.e. clients would not have to query the OCSP responder. + + OCSP Stapling enablement on Apache implicitly depends on + SSLCertificateChainFile being set by other code. + + .. note:: This function saves the configuration + + :param ssl_vhost: Destination of traffic, an ssl enabled vhost + :type ssl_vhost: :class:`~letsencrypt_apache.obj.VirtualHost` + + :param unused_options: Not currently used + :type unused_options: Not Available + + :returns: Success, general_vhost (HTTP vhost) + :rtype: (bool, :class:`~letsencrypt_apache.obj.VirtualHost`) + + """ + min_apache_ver = (2, 3, 3) + if self.get_version() < min_apache_ver: + raise errors.PluginError( + "Unable to set OCSP directives.\n" + "Apache version is below 2.3.3.") + + if "socache_shmcb_module" not in self.parser.modules: + self.enable_mod("socache_shmcb") + + # Check if there's an existing SSLUseStapling directive on. + use_stapling_aug_path = self.parser.find_dir("SSLUseStapling", + "on", start=ssl_vhost.path) + if not use_stapling_aug_path: + self.parser.add_dir(ssl_vhost.path, "SSLUseStapling", "on") + + ssl_vhost_aug_path = parser.get_aug_path(ssl_vhost.filep) + + # Check if there's an existing SSLStaplingCache directive. + stapling_cache_aug_path = self.parser.find_dir('SSLStaplingCache', + None, ssl_vhost_aug_path) + + # We'll simply delete the directive, so that we'll have a + # consistent OCSP cache path. + if stapling_cache_aug_path: + self.aug.remove( + re.sub(r"/\w*$", "", stapling_cache_aug_path[0])) + + self.parser.add_dir_to_ifmodssl(ssl_vhost_aug_path, + "SSLStaplingCache", + ["shmcb:/var/run/apache2/stapling_cache(128000)"]) + + msg = "OCSP Stapling was enabled on SSL Vhost: %s.\n"%( + ssl_vhost.filep) + self.save_notes += msg + self.save() + logger.info(msg) + def _set_http_header(self, ssl_vhost, header_substring): """Enables header that is identified by header_substring on ssl_vhost. diff --git a/certbot-apache/certbot_apache/tests/configurator_test.py b/certbot-apache/certbot_apache/tests/configurator_test.py index f2f78c8f9..4d07d1fb1 100644 --- a/certbot-apache/certbot_apache/tests/configurator_test.py +++ b/certbot-apache/certbot_apache/tests/configurator_test.py @@ -15,6 +15,7 @@ from certbot import errors from certbot.tests import acme_util from certbot_apache import configurator +from certbot_apache import parser from certbot_apache import obj from certbot_apache.tests import util @@ -85,7 +86,8 @@ class MultipleVhostsTest(util.ApacheTest): mock_getutility.notification = mock.MagicMock(return_value=True) names = self.config.get_all_names() self.assertEqual(names, set( - ["certbot.demo", "encryption-example.demo", "ip-172-30-0-17", "*.blue.purple.com"])) + ["certbot.demo", "ocspvhost.com", "encryption-example.demo", + "ip-172-30-0-17", "*.blue.purple.com"])) @mock.patch("zope.component.getUtility") @mock.patch("certbot_apache.configurator.socket.gethostbyaddr") @@ -100,14 +102,24 @@ class MultipleVhostsTest(util.ApacheTest): obj.Addr(("zombo.com",)), obj.Addr(("192.168.1.2"))]), True, False) + self.config.vhosts.append(vhost) names = self.config.get_all_names() - self.assertEqual(len(names), 6) + self.assertEqual(len(names), 7) self.assertTrue("zombo.com" in names) self.assertTrue("google.com" in names) self.assertTrue("certbot.demo" in names) + def test_bad_servername_alias(self): + ssl_vh1 = obj.VirtualHost( + "fp1", "ap1", set([obj.Addr(("*", "443"))]), + True, False) + # pylint: disable=protected-access + self.config._add_servernames(ssl_vh1) + self.assertTrue( + self.config._add_servername_alias("oy_vey", ssl_vh1) is None) + def test_add_servernames_alias(self): self.config.parser.add_dir( self.vh_truth[2].path, "ServerAlias", ["*.le.co"]) @@ -124,7 +136,7 @@ class MultipleVhostsTest(util.ApacheTest): """ vhs = self.config.get_virtual_hosts() - self.assertEqual(len(vhs), 7) + self.assertEqual(len(vhs), 8) found = 0 for vhost in vhs: @@ -135,7 +147,7 @@ class MultipleVhostsTest(util.ApacheTest): else: raise Exception("Missed: %s" % vhost) # pragma: no cover - self.assertEqual(found, 7) + self.assertEqual(found, 8) # Handle case of non-debian layout get_virtual_hosts with mock.patch( @@ -143,7 +155,7 @@ class MultipleVhostsTest(util.ApacheTest): ) as mock_conf: mock_conf.return_value = False vhs = self.config.get_virtual_hosts() - self.assertEqual(len(vhs), 7) + self.assertEqual(len(vhs), 8) @mock.patch("certbot_apache.display_ops.select_vhost") def test_choose_vhost_none_avail(self, mock_select): @@ -224,16 +236,18 @@ class MultipleVhostsTest(util.ApacheTest): # Assume only the two default vhosts. self.config.vhosts = [ vh for vh in self.config.vhosts - if vh.name not in ["certbot.demo", "encryption-example.demo"] + if vh.name not in ["certbot.demo", + "encryption-example.demo", + "ocspvhost.com"] and "*.blue.purple.com" not in vh.aliases ] - self.assertEqual( - self.config._find_best_vhost("example.demo"), self.vh_truth[2]) + self.config._find_best_vhost("encryption-example.demo"), + self.vh_truth[2]) def test_non_default_vhosts(self): # pylint: disable=protected-access - self.assertEqual(len(self.config._non_default_vhosts()), 5) + self.assertEqual(len(self.config._non_default_vhosts()), 6) def test_is_site_enabled(self): """Test if site is enabled. @@ -539,7 +553,7 @@ class MultipleVhostsTest(util.ApacheTest): self.assertEqual(self.config.is_name_vhost(self.vh_truth[0]), self.config.is_name_vhost(ssl_vhost)) - self.assertEqual(len(self.config.vhosts), 8) + self.assertEqual(len(self.config.vhosts), 9) def test_clean_vhost_ssl(self): # pylint: disable=protected-access @@ -726,16 +740,15 @@ class MultipleVhostsTest(util.ApacheTest): def test_get_all_certs_keys(self): c_k = self.config.get_all_certs_keys() - - self.assertEqual(len(c_k), 2) + self.assertEqual(len(c_k), 3) cert, key, path = next(iter(c_k)) self.assertTrue("cert" in cert) self.assertTrue("key" in key) - self.assertTrue("default-ssl" in path) + self.assertTrue("default-ssl" in path or "ocsp-ssl" in path) def test_get_all_certs_keys_malformed_conf(self): self.config.parser.find_dir = mock.Mock( - side_effect=[["path"], [], ["path"], []]) + side_effect=[["path"], [], ["path"], [], ["path"], []]) c_k = self.config.get_all_certs_keys() self.assertFalse(c_k) @@ -756,15 +769,20 @@ class MultipleVhostsTest(util.ApacheTest): def test_supported_enhancements(self): self.assertTrue(isinstance(self.config.supported_enhancements(), list)) + @mock.patch("certbot_apache.configurator.ApacheConfigurator._get_http_vhost") + @mock.patch("certbot_apache.display_ops.select_vhost") @mock.patch("certbot.le_util.exe_exists") - def test_enhance_unknown_vhost(self, mock_exe): + def test_enhance_unknown_vhost(self, mock_exe, mock_sel_vhost, mock_get): self.config.parser.modules.add("rewrite_module") mock_exe.return_value = True - ssl_vh = obj.VirtualHost( - "fp", "ap", set([obj.Addr(("*", "443"))]), + ssl_vh1 = obj.VirtualHost( + "fp1", "ap1", set([obj.Addr(("*", "443"))]), True, False) - ssl_vh.name = "satoshi.com" - self.config.vhosts.append(ssl_vh) + ssl_vh1.name = "satoshi.com" + self.config.vhosts.append(ssl_vh1) + mock_sel_vhost.return_value = None + mock_get.return_value = None + self.assertRaises( errors.PluginError, self.config.enhance, "satoshi.com", "redirect") @@ -774,6 +792,85 @@ class MultipleVhostsTest(util.ApacheTest): errors.PluginError, self.config.enhance, "certbot.demo", "unknown_enhancement") + @mock.patch("certbot.le_util.run_script") + @mock.patch("certbot.le_util.exe_exists") + def test_ocsp_stapling(self, mock_exe, mock_run_script): + self.config.parser.update_runtime_variables = mock.Mock() + self.config.parser.modules.add("mod_ssl.c") + self.config.get_version = mock.Mock(return_value=(2, 4, 7)) + mock_exe.return_value = True + + # This will create an ssl vhost for certbot.demo + self.config.enhance("certbot.demo", "staple-ocsp") + + self.assertTrue("socache_shmcb_module" in self.config.parser.modules) + self.assertTrue(mock_run_script.called) + + # Get the ssl vhost for certbot.demo + ssl_vhost = self.config.assoc["certbot.demo"] + + ssl_use_stapling_aug_path = self.config.parser.find_dir( + "SSLUseStapling", "on", ssl_vhost.path) + + self.assertEqual(len(ssl_use_stapling_aug_path), 1) + + ssl_vhost_aug_path = parser.get_aug_path(ssl_vhost.filep) + stapling_cache_aug_path = self.config.parser.find_dir('SSLStaplingCache', + "shmcb:/var/run/apache2/stapling_cache(128000)", + ssl_vhost_aug_path) + + self.assertEqual(len(stapling_cache_aug_path), 1) + + @mock.patch("certbot.le_util.exe_exists") + def test_ocsp_stapling_twice(self, mock_exe): + self.config.parser.update_runtime_variables = mock.Mock() + self.config.parser.modules.add("mod_ssl.c") + self.config.parser.modules.add("socache_shmcb_module") + self.config.get_version = mock.Mock(return_value=(2, 4, 7)) + mock_exe.return_value = True + + # Checking the case with already enabled ocsp stapling configuration + self.config.enhance("ocspvhost.com", "staple-ocsp") + + # Get the ssl vhost for letsencrypt.demo + ssl_vhost = self.config.assoc["ocspvhost.com"] + + ssl_use_stapling_aug_path = self.config.parser.find_dir( + "SSLUseStapling", "on", ssl_vhost.path) + + self.assertEqual(len(ssl_use_stapling_aug_path), 1) + + ssl_vhost_aug_path = parser.get_aug_path(ssl_vhost.filep) + stapling_cache_aug_path = self.config.parser.find_dir('SSLStaplingCache', + "shmcb:/var/run/apache2/stapling_cache(128000)", + ssl_vhost_aug_path) + + self.assertEqual(len(stapling_cache_aug_path), 1) + + + @mock.patch("certbot.le_util.exe_exists") + def test_ocsp_unsupported_apache_version(self, mock_exe): + mock_exe.return_value = True + self.config.parser.update_runtime_variables = mock.Mock() + self.config.parser.modules.add("mod_ssl.c") + self.config.parser.modules.add("socache_shmcb_module") + self.config.get_version = mock.Mock(return_value=(2, 2, 0)) + + self.assertRaises(errors.PluginError, + self.config.enhance, "certbot.demo", "staple-ocsp") + + + def test_get_http_vhost_third_filter(self): + ssl_vh = obj.VirtualHost( + "fp", "ap", set([obj.Addr(("*", "443"))]), + True, False) + ssl_vh.name = "satoshi.com" + self.config.vhosts.append(ssl_vh) + + # pylint: disable=protected-access + http_vh = self.config._get_http_vhost(ssl_vh) + self.assertTrue(http_vh.ssl == False) + @mock.patch("certbot.le_util.run_script") @mock.patch("certbot.le_util.exe_exists") def test_http_header_hsts(self, mock_exe, _): @@ -899,7 +996,7 @@ class MultipleVhostsTest(util.ApacheTest): def test_redirect_with_existing_rewrite(self, mock_exe, _): self.config.parser.update_runtime_variables = mock.Mock() mock_exe.return_value = True - self.config.get_version = mock.Mock(return_value=(2, 2)) + self.config.get_version = mock.Mock(return_value=(2, 2, 0)) # Create a preexisting rewrite rule self.config.parser.add_dir( @@ -957,7 +1054,7 @@ class MultipleVhostsTest(util.ApacheTest): # pylint: disable=protected-access self.config._enable_redirect(self.vh_truth[1], "") - self.assertEqual(len(self.config.vhosts), 8) + self.assertEqual(len(self.config.vhosts), 9) def test_create_own_redirect_for_old_apache_version(self): self.config.parser.modules.add("rewrite_module") @@ -968,7 +1065,7 @@ class MultipleVhostsTest(util.ApacheTest): # pylint: disable=protected-access self.config._enable_redirect(self.vh_truth[1], "") - self.assertEqual(len(self.config.vhosts), 8) + self.assertEqual(len(self.config.vhosts), 9) def test_sift_line(self): # pylint: disable=protected-access diff --git a/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/multiple_vhosts/apache2/sites-available/ocsp-ssl.conf b/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/multiple_vhosts/apache2/sites-available/ocsp-ssl.conf new file mode 100644 index 000000000..631cf16c8 --- /dev/null +++ b/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/multiple_vhosts/apache2/sites-available/ocsp-ssl.conf @@ -0,0 +1,36 @@ + +SSLStaplingCache shmcb:/var/run/apache2/stapling_cache(128000) + + # The ServerName directive sets the request scheme, hostname and port that + # the server uses to identify itself. This is used when creating + # redirection URLs. In the context of virtual hosts, the ServerName + # specifies what hostname must appear in the request's Host: header to + # match this virtual host. For the default virtual host (this file) this + # value is not decisive as it is used as a last resort host regardless. + # However, you must set it for any further virtual host explicitly. + ServerName ocspvhost.com + + ServerAdmin webmaster@dumpbits.com + DocumentRoot /var/www/html + + # Available loglevels: trace8, ..., trace1, debug, info, notice, warn, + # error, crit, alert, emerg. + # It is also possible to configure the loglevel for particular + # modules, e.g. + #LogLevel info ssl:warn + + ErrorLog ${APACHE_LOG_DIR}/error.log + CustomLog ${APACHE_LOG_DIR}/access.log combined + + # For most configuration files from conf-available/, which are + # enabled or disabled at a global level, it is possible to + # include a line for only one particular virtual host. For example the + # following line enables the CGI configuration for this host only + # after it has been globally disabled with "a2disconf". + #Include conf-available/serve-cgi-bin.conf +SSLCertificateFile /etc/apache2/certs/certbot-cert_5.pem +SSLCertificateKeyFile /etc/apache2/ssl/key-certbot_15.pem +SSLUseStapling on + +# vim: syntax=apache ts=4 sw=4 sts=4 sr noet + diff --git a/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/multiple_vhosts/apache2/sites-enabled/ocsp-ssl.conf b/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/multiple_vhosts/apache2/sites-enabled/ocsp-ssl.conf new file mode 120000 index 000000000..b25ee0482 --- /dev/null +++ b/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/multiple_vhosts/apache2/sites-enabled/ocsp-ssl.conf @@ -0,0 +1 @@ +../sites-available/ocsp-ssl.conf \ No newline at end of file diff --git a/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/multiple_vhosts/sites b/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/multiple_vhosts/sites index 06bf6a2ae..ab518ee5b 100644 --- a/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/multiple_vhosts/sites +++ b/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/multiple_vhosts/sites @@ -1,2 +1,3 @@ sites-available/certbot.conf, certbot.demo sites-available/encryption-example.conf, encryption-example.demo +sites-available/ocsp-ssl.conf, ocspvhost.com diff --git a/certbot-apache/certbot_apache/tests/util.py b/certbot-apache/certbot_apache/tests/util.py index 9fb5dcdfa..8935ee908 100644 --- a/certbot-apache/certbot_apache/tests/util.py +++ b/certbot-apache/certbot_apache/tests/util.py @@ -156,8 +156,12 @@ def get_vh_truth(temp_dir, config_name): os.path.join(prefix, "wildcard.conf"), os.path.join(aug_pre, "wildcard.conf/VirtualHost"), set([obj.Addr.fromstring("*:80")]), False, False, - "ip-172-30-0-17", aliases=["*.blue.purple.com"]) - ] + "ip-172-30-0-17", aliases=["*.blue.purple.com"]), + obj.VirtualHost( + os.path.join(prefix, "ocsp-ssl.conf"), + os.path.join(aug_pre, "ocsp-ssl.conf/IfModule/VirtualHost"), + set([obj.Addr.fromstring("10.2.3.4:443")]), True, True, + "ocspvhost.com")] return vh_truth return None # pragma: no cover diff --git a/certbot/cli.py b/certbot/cli.py index e15725ece..5dbad3ed4 100644 --- a/certbot/cli.py +++ b/certbot/cli.py @@ -731,9 +731,20 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): " https:// for every http:// resource.", dest="uir", default=None) helpful.add( "security", "--no-uir", action="store_false", - help=" Do not automatically set the \"Content-Security-Policy:" + help="Do not automatically set the \"Content-Security-Policy:" " upgrade-insecure-requests\" header to every HTTP response.", dest="uir", default=None) + helpful.add( + "security", "--staple-ocsp", action="store_true", + help="Enables OCSP Stapling. A valid OCSP response is stapled to" + " the certificate that the server offers during TLS.", + dest="staple", default=None) + helpful.add( + "security", "--no-staple-ocsp", action="store_false", + help="Do not automatically enable OCSP Stapling.", + dest="staple", default=None) + + helpful.add( "security", "--strict-permissions", action="store_true", help="Require that all configuration files are owned by the current " diff --git a/certbot/client.py b/certbot/client.py index 6f41a3a0b..0159d3946 100644 --- a/certbot/client.py +++ b/certbot/client.py @@ -396,7 +396,8 @@ class Client(object): supported = self.installer.supported_enhancements() redirect = config.redirect if "redirect" in supported else False hsts = config.hsts if "ensure-http-header" in supported else False - uir = config.uir if "ensure-http-header" in supported else False + uir = config.uir if "ensure-http-header" in supported else False + staple = config.staple if "staple-ocsp" in supported else False if redirect is None: redirect = enhancements.ask("redirect") @@ -410,9 +411,11 @@ class Client(object): if uir: self.apply_enhancement(domains, "ensure-http-header", "Upgrade-Insecure-Requests") + if staple: + self.apply_enhancement(domains, "staple-ocsp") msg = ("We were unable to restart web server") - if redirect or hsts or uir: + if redirect or hsts or uir or staple: with error_handler.ErrorHandler(self._rollback_and_restart, msg): self.installer.restart() From 22badb2380bc405032d47e9f90212a0f1a507fad Mon Sep 17 00:00:00 2001 From: Noah Swartz Date: Thu, 19 May 2016 17:29:39 -0700 Subject: [PATCH 154/396] tests pass? --- letsencrypt-auto-source/tests/auto_test.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/letsencrypt-auto-source/tests/auto_test.py b/letsencrypt-auto-source/tests/auto_test.py index 6fccdb56e..7e131f4cf 100644 --- a/letsencrypt-auto-source/tests/auto_test.py +++ b/letsencrypt-auto-source/tests/auto_test.py @@ -199,7 +199,7 @@ iQIDAQAB **kwargs) env.update(d) return out_and_err( - join(venv_dir, 'certbot-auto') + ' --version', + join(venv_dir, 'letsencrypt-auto') + ' --version', shell=True, env=env) @@ -259,8 +259,8 @@ class AutoTests(TestCase): # This serves a PyPI page with a higher version, a GitHub-alike # with a corresponding le-auto script, and a matching signature. resources = {'certbot/json': dumps({'releases': {'99.9.9': None}}), - 'v99.9.9/certbot-auto': NEW_LE_AUTO, - 'v99.9.9/certbot-auto.sig': NEW_LE_AUTO_SIG} + 'v99.9.9/letsencrypt-auto': NEW_LE_AUTO, + 'v99.9.9/letsencrypt-auto.sig': NEW_LE_AUTO_SIG} with serving(resources) as base_url: run_letsencrypt_auto = partial( run_le_auto, @@ -303,8 +303,8 @@ class AutoTests(TestCase): # making a bad key, and a mismatch is a mismatch): resources = {'': 'certbot/', 'certbot/json': dumps({'releases': {'99.9.9': None}}), - 'v99.9.9/certbot-auto': build_le_auto(version='99.9.9'), - 'v99.9.9/certbot-auto.sig': signed('something else')} + 'v99.9.9/letsencrypt-auto': build_le_auto(version='99.9.9'), + 'v99.9.9/letsencrypt-auto.sig': signed('something else')} with serving(resources) as base_url: copy(LE_AUTO_PATH, venv_dir) try: From 7689de2ad8da074015df2150b455115f3b5816b5 Mon Sep 17 00:00:00 2001 From: sagi Date: Fri, 20 May 2016 01:18:50 +0000 Subject: [PATCH 155/396] Fix tests --- certbot/client.py | 12 +++++++++++- certbot/tests/client_test.py | 8 +++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/certbot/client.py b/certbot/client.py index 4fc662948..818701f08 100644 --- a/certbot/client.py +++ b/certbot/client.py @@ -569,12 +569,22 @@ def view_config_changes(config, num=None): rev.view_config_changes(num) def _open_pem_file(cli_arg_path, pem_path): + """Open a pem file. + + If cli_arg_path was set by the client, open that. + Otherwise, uniquify the file path. + + :param str cli_arg_path: the cli arg name, e.g. cert_path + :param str pem_path: the pem file path to open + + :returns a file object + """ if cli.set_by_cli(cli_arg_path): return le_util.safe_open(pem_path, chmod=0o644),\ os.path.abspath(pem_path) else: uniq = le_util.unique_file(pem_path, 0o644) - return uniq[0], os.path.abspath(uniq) + return uniq[0], os.path.abspath(uniq[1]) def _save_chain(chain_pem, chain_file): """Saves chain_pem at a unique path based on chain_path. diff --git a/certbot/tests/client_test.py b/certbot/tests/client_test.py index 8ceefe8ae..49596fdee 100644 --- a/certbot/tests/client_test.py +++ b/certbot/tests/client_test.py @@ -221,7 +221,9 @@ class ClientTest(unittest.TestCase): mock.sentinel.key, domains, self.config.csr_dir) self._check_obtain_certificate() - def test_save_certificate(self): + @mock.patch("certbot.cli.helpful_parser") + def test_save_certificate(self, mock_parser): + # pylint: disable=too-many-locals certs = ["matching_cert.pem", "cert.pem", "cert-san.pem"] tmp_path = tempfile.mkdtemp() os.chmod(tmp_path, 0o755) # TODO: really?? @@ -232,6 +234,10 @@ class ClientTest(unittest.TestCase): candidate_cert_path = os.path.join(tmp_path, "certs", "cert.pem") candidate_chain_path = os.path.join(tmp_path, "chains", "chain.pem") candidate_fullchain_path = os.path.join(tmp_path, "chains", "fullchain.pem") + mock_parser.verb = "certonly" + mock_parser.args = ["--cert-path", candidate_cert_path, + "--chain-path", candidate_chain_path, + "--fullchain-path", candidate_fullchain_path] cert_path, chain_path, fullchain_path = self.client.save_certificate( certr, chain_cert, candidate_cert_path, candidate_chain_path, From 46be2df1999e65e49e6194484f7bee5fea894fcb Mon Sep 17 00:00:00 2001 From: Noah Swartz Date: Fri, 20 May 2016 13:25:34 -0700 Subject: [PATCH 156/396] fix syntax error --- certbot/reverter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certbot/reverter.py b/certbot/reverter.py index fe6d9f24f..b023d18a7 100644 --- a/certbot/reverter.py +++ b/certbot/reverter.py @@ -489,7 +489,7 @@ class Reverter(object): if not os.path.exists(changes_since_path): logger.info("Rollback checkpoint is empty (no changes made?)") - with open(self.config.changes_since_path) as f: + with open(changes_since_path, 'w') as f: f.write("No changes\n") # Add title to self.config.in_progress_dir CHANGES_SINCE From 32d32dbc1279603a8cdc0cef7e0a0c42563f71ba Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Fri, 20 May 2016 15:20:28 -0700 Subject: [PATCH 157/396] cli.py PEP8 fixes --- certbot/cli.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/certbot/cli.py b/certbot/cli.py index af4cd3013..3bd565496 100644 --- a/certbot/cli.py +++ b/certbot/cli.py @@ -347,7 +347,6 @@ class HelpfulArgumentParser(object): return parsed_args - def set_test_server(self, parsed_args): """We have --staging/--dry-run; perform sanity check and set config.server""" @@ -370,7 +369,6 @@ class HelpfulArgumentParser(object): parsed_args.tos = True parsed_args.register_unsafely_without_email = True - def handle_csr(self, parsed_args): """Process a --csr flag.""" if parsed_args.verb != "certonly": From 73c4e8f7a40b34e8dfed12d0e784e1aef0d25523 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Fri, 20 May 2016 16:24:49 -0700 Subject: [PATCH 158/396] Cleanup test_obtain_certificate_from_csr --- certbot/tests/client_test.py | 74 +++++++++++++----------------------- 1 file changed, 27 insertions(+), 47 deletions(-) diff --git a/certbot/tests/client_test.py b/certbot/tests/client_test.py index 148857be7..b7195a777 100644 --- a/certbot/tests/client_test.py +++ b/certbot/tests/client_test.py @@ -134,59 +134,39 @@ class ClientTest(unittest.TestCase): self.acme.fetch_chain.assert_called_once_with(mock.sentinel.certr) - # FIXME move parts of this to crypto_util tests... @mock.patch("certbot.client.logger") def test_obtain_certificate_from_csr(self, mock_logger): self._mock_obtain_certificate() - from certbot import cli test_csr = le_util.CSR(form="der", file=None, data=CSR_SAN) - mock_parsed_args = mock.MagicMock() - # The CLI should believe that this is a certonly request, because - # a CSR would not be allowed with other kinds of requests! - mock_parsed_args.verb = "certonly" - with mock.patch("certbot.cli.crypto_util.le_util.CSR") as mock_CSR: - mock_CSR.return_value = test_csr - mock_parsed_args.domains = self.eg_domains[:] - mock_parsed_args.allow_subset_of_names = False - mock_parsed_args.csr = (mock.MagicMock(), mock.MagicMock()) - mock_parser = mock.MagicMock(cli.HelpfulArgumentParser) - cli.HelpfulArgumentParser.handle_csr(mock_parser, mock_parsed_args) + auth_handler = self.client.auth_handler - # Now provoke an inconsistent domains error... - mock_parsed_args.domains.append("hippopotamus.io") - self.assertRaises(errors.ConfigurationError, - cli.HelpfulArgumentParser.handle_csr, mock_parser, mock_parsed_args) - - authzr = self.client.auth_handler.get_authorizations(self.eg_domains, False) - - self.assertEqual( - (mock.sentinel.certr, mock.sentinel.chain), - self.client.obtain_certificate_from_csr( - self.eg_domains, - test_csr, - authzr=authzr)) - # and that the cert was obtained correctly - self._check_obtain_certificate() - - # Test for authzr=None - self.assertEqual( - (mock.sentinel.certr, mock.sentinel.chain), - self.client.obtain_certificate_from_csr( - self.eg_domains, - test_csr, - authzr=None)) - - self.client.auth_handler.get_authorizations.assert_called_with( - self.eg_domains) - - # Test for no auth_handler - self.client.auth_handler = None - self.assertRaises( - errors.Error, - self.client.obtain_certificate_from_csr, + authzr = auth_handler.get_authorizations(self.eg_domains, False) + self.assertEqual( + (mock.sentinel.certr, mock.sentinel.chain), + self.client.obtain_certificate_from_csr( self.eg_domains, - test_csr) - mock_logger.warning.assert_called_once_with(mock.ANY) + test_csr, + authzr=authzr)) + # and that the cert was obtained correctly + self._check_obtain_certificate() + + # Test for authzr=None + self.assertEqual( + (mock.sentinel.certr, mock.sentinel.chain), + self.client.obtain_certificate_from_csr( + self.eg_domains, + test_csr, + authzr=None)) + auth_handler.get_authorizations.assert_called_with(self.eg_domains) + + # Test for no auth_handler + self.client.auth_handler = None + self.assertRaises( + errors.Error, + self.client.obtain_certificate_from_csr, + self.eg_domains, + test_csr) + mock_logger.warning.assert_called_once_with(mock.ANY) @mock.patch("certbot.client.crypto_util") def test_obtain_certificate(self, mock_crypto_util): From 53286863fefa47b7464340b8b46bbd93b5358932 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Fri, 20 May 2016 16:26:09 -0700 Subject: [PATCH 159/396] Simplify import_csr_file --- certbot/crypto_util.py | 31 +++++++++++++------------------ 1 file changed, 13 insertions(+), 18 deletions(-) diff --git a/certbot/crypto_util.py b/certbot/crypto_util.py index ba368b15b..68e07e059 100644 --- a/certbot/crypto_util.py +++ b/certbot/crypto_util.py @@ -180,33 +180,28 @@ def csr_matches_pubkey(csr, privkey): return False -def import_csr_file(csrfile, contents): +def import_csr_file(csrfile, data): """Import a CSR file, which can be either PEM or DER. :param str csrfile: CSR filename - :param str contents: contens of the CSR file + :param str data: contents of the CSR file - :returns: (le_util.CSR object representing the CSR, - `OpenSSL.crypto.FILETYPE_PEM` or `OpenSSL.crypto.FILETYPE_ASN1`, + :returns: (`OpenSSL.crypto.FILETYPE_PEM` or `OpenSSL.crypto.FILETYPE_ASN1`, + le_util.CSR object representing the CSR, list of domains requested in the CSR) - :rtype: tuple + """ - try: - csr = le_util.CSR(file=csrfile, data=contents, form="der") - typ = OpenSSL.crypto.FILETYPE_ASN1 - domains = get_sans_from_csr(csr.data, OpenSSL.crypto.FILETYPE_ASN1) - except OpenSSL.crypto.Error: + for form, typ in (("der", OpenSSL.crypto.FILETYPE_ASN1,), + ("pem", OpenSSL.crypto.FILETYPE_PEM,),): try: - e1 = traceback.format_exc() - typ = OpenSSL.crypto.FILETYPE_PEM - csr = le_util.CSR(file=csrfile, data=contents, form="pem") - domains = get_sans_from_csr(csr.data, typ) + domains = get_names_from_csr(data, typ) except OpenSSL.crypto.Error: - logger.debug("DER CSR parse error %s", e1) - logger.debug("PEM CSR parse error %s", traceback.format_exc()) - raise errors.Error("Failed to parse CSR file: {0}".format(csrfile)) - return typ, csr, domains + logger.debug("CSR parse error (form=%s, typ=%s):", form, typ) + logger.debug(traceback.format_exc()) + continue + return typ, le_util.CSR(file=csrfile, data=data, form=form), domains + raise errors.Error("Failed to parse CSR file: {0}".format(csrfile)) def make_key(bits): From 271316a41343593e598f721f3aee3e1621b987e9 Mon Sep 17 00:00:00 2001 From: Noah Swartz Date: Fri, 20 May 2016 16:32:39 -0700 Subject: [PATCH 160/396] cover test --- certbot/reverter.py | 1 + certbot/tests/reverter_test.py | 14 ++++++++++++++ 2 files changed, 15 insertions(+) diff --git a/certbot/reverter.py b/certbot/reverter.py index b023d18a7..16ee5d8a4 100644 --- a/certbot/reverter.py +++ b/certbot/reverter.py @@ -7,6 +7,7 @@ import shutil import time import traceback + import zope.component from certbot import constants diff --git a/certbot/tests/reverter_test.py b/certbot/tests/reverter_test.py index eda5ffb36..c79c72a48 100644 --- a/certbot/tests/reverter_test.py +++ b/certbot/tests/reverter_test.py @@ -34,6 +34,20 @@ class ReverterCheckpointLocalTest(unittest.TestCase): logging.disable(logging.NOTSET) + @mock.patch("certbot.reverter.Reverter._read_and_append") + def test_no_change(self, mock_read): + mock_read.side_effect = OSError("cannot even") + try: + self.reverter.add_to_checkpoint(self.sets[0], "save1") + except: + pass + self.reverter.finalize_checkpoint("blah") + path = os.listdir(self.reverter.config.backup_dir)[0] + no_change = os.path.join(self.reverter.config.backup_dir, path, "CHANGES_SINCE") + with open(no_change, "r") as f: + x = f.read() + self.assertTrue("No changes" in x) + def test_basic_add_to_temp_checkpoint(self): # These shouldn't conflict even though they are both named config.txt self.reverter.add_to_temp_checkpoint(self.sets[0], "save1") From a48afd498c9c7a022eff72d8f903879f824cbe14 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Fri, 20 May 2016 16:52:35 -0700 Subject: [PATCH 161/396] Start import_csr_file tests --- certbot/tests/crypto_util_test.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/certbot/tests/crypto_util_test.py b/certbot/tests/crypto_util_test.py index eade4861f..af8308fd8 100644 --- a/certbot/tests/crypto_util_test.py +++ b/certbot/tests/crypto_util_test.py @@ -10,6 +10,7 @@ import zope.component from certbot import errors from certbot import interfaces +from certbot import le_util from certbot.tests import test_util @@ -159,6 +160,27 @@ class CSRMatchesPubkeyTest(unittest.TestCase): test_util.load_vector('csr.pem'), RSA256_KEY)) +class ImportCSRFileTest(unittest.TestCase): + """Tests for certbot.certbot_util.import_csr_file.""" + + @classmethod + def _call(cls, *args, **kwargs): + from certbot.crypto_util import import_csr_file + return import_csr_file(*args, **kwargs) + + def test_der_csr(self): + csrfile = test_util.vector_path('csr.der') + data = test_util.load_vector('csr.der') + + self.assertEqual( + (OpenSSL.crypto.FILETYPE_ASN1, + le_util.CSR(file=csrfile, + data=data, + form="der"), + ["example.com"],), + self._call(csrfile, data)) + + class MakeKeyTest(unittest.TestCase): # pylint: disable=too-few-public-methods """Tests for certbot.crypto_util.make_key.""" From 953d4957b8e32a74d894c2d3eeab56e0105ebf94 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Fri, 20 May 2016 16:57:13 -0700 Subject: [PATCH 162/396] Add csr test --- certbot/tests/crypto_util_test.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/certbot/tests/crypto_util_test.py b/certbot/tests/crypto_util_test.py index af8308fd8..d5a016320 100644 --- a/certbot/tests/crypto_util_test.py +++ b/certbot/tests/crypto_util_test.py @@ -180,6 +180,18 @@ class ImportCSRFileTest(unittest.TestCase): ["example.com"],), self._call(csrfile, data)) + def test_pem_csr(self): + csrfile = test_util.vector_path('csr.pem') + data = test_util.load_vector('csr.pem') + + self.assertEqual( + (OpenSSL.crypto.FILETYPE_PEM, + le_util.CSR(file=csrfile, + data=data, + form="pem"), + ["example.com"],), + self._call(csrfile, data)) + class MakeKeyTest(unittest.TestCase): # pylint: disable=too-few-public-methods """Tests for certbot.crypto_util.make_key.""" From 2c4c8c081c80d00be04149afd7da2396f52a0a93 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Fri, 20 May 2016 17:00:06 -0700 Subject: [PATCH 163/396] Test bad csr --- certbot/tests/crypto_util_test.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/certbot/tests/crypto_util_test.py b/certbot/tests/crypto_util_test.py index d5a016320..eeea0f4ab 100644 --- a/certbot/tests/crypto_util_test.py +++ b/certbot/tests/crypto_util_test.py @@ -192,6 +192,11 @@ class ImportCSRFileTest(unittest.TestCase): ["example.com"],), self._call(csrfile, data)) + def test_bad_csr(self): + self.assertRaises(errors.Error, self._call, + test_util.vector_path('cert.pem'), + test_util.load_vector('cert.pem')) + class MakeKeyTest(unittest.TestCase): # pylint: disable=too-few-public-methods """Tests for certbot.crypto_util.make_key.""" From 3c11733006b03da942d0ee43a8d16c1c6bd40d77 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Fri, 20 May 2016 17:32:37 -0700 Subject: [PATCH 164/396] fix --csr and --allow-subset-of-names test --- certbot/tests/cli_test.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/certbot/tests/cli_test.py b/certbot/tests/cli_test.py index d7965a24e..62ec36d2a 100644 --- a/certbot/tests/cli_test.py +++ b/certbot/tests/cli_test.py @@ -349,8 +349,9 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods ['-d', '204.11.231.35']) def test_csr_with_besteffort(self): - args = ["--csr", CSR, "--allow-subset-of-names"] - self.assertRaises(errors.Error, self._call, args) + self.assertRaises( + errors.Error, self._call, + 'certonly --csr {0} --allow-subset-of-names'.format(CSR).split()) def test_run_with_csr(self): # This is an error because you can only use --csr with certonly From 0b85a8f1c8d26accccd148701f2e7ddd6065a6e2 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Fri, 20 May 2016 17:37:48 -0700 Subject: [PATCH 165/396] Add a test for a CSR with no domains --- certbot/tests/cli_test.py | 6 ++++++ certbot/tests/testdata/csr-nonames.pem | 8 ++++++++ 2 files changed, 14 insertions(+) create mode 100644 certbot/tests/testdata/csr-nonames.pem diff --git a/certbot/tests/cli_test.py b/certbot/tests/cli_test.py index 62ec36d2a..c24da4989 100644 --- a/certbot/tests/cli_test.py +++ b/certbot/tests/cli_test.py @@ -362,6 +362,12 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods return assert False, "Expected supplying --csr to fail with default verb" + def test_csr_with_no_domains(self): + self.assertRaises( + errors.Error, self._call, + 'certonly --csr {0}'.format( + test_util.vector_path('csr-nonames.pem')).split()) + def _get_argument_parser(self): plugins = disco.PluginsRegistry.find_all() return functools.partial(cli.prepare_and_parse_args, plugins) diff --git a/certbot/tests/testdata/csr-nonames.pem b/certbot/tests/testdata/csr-nonames.pem new file mode 100644 index 000000000..abe1029ca --- /dev/null +++ b/certbot/tests/testdata/csr-nonames.pem @@ -0,0 +1,8 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIH/MIGqAgEAMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw +HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwXDANBgkqhkiG9w0BAQEF +AANLADBIAkEArHVztFHtH92ucFJD/N/HW9AsdRsUuHUBBBDlHwNlRd3fp580rv2+ +6QWE30cWgdmJS86ObRz6lUTor4R0T+3C5QIDAQABoAAwDQYJKoZIhvcNAQELBQAD +QQBt9XLSZ9DGfWcGGaBUTCiSY7lWBegpNlCeo8pK3ydWmKpjcza+j7lF5paph2LH +lKWVQ8+xwYMscGWK0NApHGco +-----END CERTIFICATE REQUEST----- From 5cb0e0f264ef94187b774561329d2ab432e2634e Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Fri, 20 May 2016 17:41:58 -0700 Subject: [PATCH 166/396] Add test for csr with inconsistent domains --- certbot/tests/cli_test.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/certbot/tests/cli_test.py b/certbot/tests/cli_test.py index c24da4989..4ae69e69d 100644 --- a/certbot/tests/cli_test.py +++ b/certbot/tests/cli_test.py @@ -368,6 +368,11 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods 'certonly --csr {0}'.format( test_util.vector_path('csr-nonames.pem')).split()) + def test_csr_with_inconsistent_domains(self): + self.assertRaises( + errors.Error, self._call, + 'certonly -d example.org --csr {0}'.format(CSR).split()) + def _get_argument_parser(self): plugins = disco.PluginsRegistry.find_all() return functools.partial(cli.prepare_and_parse_args, plugins) From 3105fe8a34618f22b906ef8f4875dbf5f1695dca Mon Sep 17 00:00:00 2001 From: Seth Schoen Date: Sat, 21 May 2016 13:26:59 -0700 Subject: [PATCH 167/396] Disable too-many-statements on parser setup --- certbot/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certbot/cli.py b/certbot/cli.py index 7898c4c0b..356e71c56 100644 --- a/certbot/cli.py +++ b/certbot/cli.py @@ -567,7 +567,7 @@ class HelpfulArgumentParser(object): return dict([(t, t == chosen_topic) for t in self.help_topics]) -def prepare_and_parse_args(plugins, args, detect_defaults=False): +def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: disable=too-many-statements """Returns parsed command line arguments. :param .PluginsRegistry plugins: available plugins From fab9f8db78a52cd7019867a4bf5748cf8f932097 Mon Sep 17 00:00:00 2001 From: Seth Schoen Date: Sat, 21 May 2016 13:27:12 -0700 Subject: [PATCH 168/396] Complete register functionality (for now) --- certbot/main.py | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/certbot/main.py b/certbot/main.py index 7b50efd0f..fc54c8615 100644 --- a/certbot/main.py +++ b/certbot/main.py @@ -370,11 +370,27 @@ def _init_le_client(config, authenticator, installer): def register(config, unused_plugins): """Create or modify accounts on the server.""" - # Currently, only --update-registration is implemented. Issue #2446 - # calls for a fuller register verb, to allow better separation of - # account management from obtaining certs. + # Portion of _determine_account logic to see whether accounts already + # exist or not. + account_storage = account.AccountFileStorage(config) + accounts = account_storage.find_all() + + # registering a new account if not config.update_registration: - return "Currently, only register --update-registration is implemented." + if len(accounts) > 0: + # TODO: add a flag to register a duplicate account (this will + # also require extending _determine_account's behavior + # or else extracting the registration code from there) + return ("There is an existing account; registration of a " + "duplicate account with this command is currently " + "unsupported.") + # _determine_account will register an account + _determine_account(config) + return + + # --update-registration + if len(accounts) == 0: + return "Could not find an existing account to update." if config.email is None: return ("Currently, --update-registration can only change the e-mail " "address\nassociated with an account. A new e-mail address is " @@ -392,6 +408,7 @@ def register(config, unused_plugins): # We rely on an ACME exception to interrupt this process if it didn't work. print("Registration change succeeded. New registration data:\n") print(query_data) + return def install(config, plugins): From e7c8b73083a33cc942edeb9d69774b18f4895c9b Mon Sep 17 00:00:00 2001 From: Seth Schoen Date: Sun, 22 May 2016 09:50:27 -0700 Subject: [PATCH 169/396] Test coverage for register verb --- certbot/tests/cli_test.py | 67 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/certbot/tests/cli_test.py b/certbot/tests/cli_test.py index d7965a24e..1b6d032a8 100644 --- a/certbot/tests/cli_test.py +++ b/certbot/tests/cli_test.py @@ -14,6 +14,7 @@ import mock import six from six.moves import reload_module # pylint: disable=import-error +from acme import errors as acme_errors from acme import jose from certbot import account @@ -888,6 +889,72 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods self._call(['-c', test_util.vector_path('cli.ini')]) self.assertTrue(mocked_run.called) + def test_register(self): + with mock.patch('certbot.main.client') as mocked_client: + acc = mock.MagicMock() + acc.id = "imaginary_account" + mocked_client.register.return_value = (acc, "worked") + self._call_no_clientmock(["register", "--email", "user@example.org"]) + # TODO: It would be more correct to explicitly check that + # _determine_account() gets called in the above case, + # but coverage statistics should also show that it did. + with mock.patch('certbot.main.account') as mocked_account: + mocked_storage = mock.MagicMock() + mocked_account.AccountFileStorage.return_value = mocked_storage + mocked_storage.find_all.return_value = ["an account"] + x = self._call_no_clientmock(["register", "--email", "user@example.org"]) + assert "There is an existing account" in x[0] + + def test_update_registration_no_existing_accounts(self): + # with mock.patch('certbot.main.client') as mocked_client: + with mock.patch('certbot.main.account') as mocked_account: + mocked_storage = mock.MagicMock() + mocked_account.AccountFileStorage.return_value = mocked_storage + mocked_storage.find_all.return_value = [] + x = self._call_no_clientmock( + ["register", "--update-registration", "--email", + "user@example.org"]) + assert "Could not find an existing account" in x[0] + + def test_update_registration_no_email(self): + # This test will become obsolete when register --update-registration + # supports updating something other than the e-mail address! + # with mock.patch('certbot.main.client') as mocked_client: + with mock.patch('certbot.main.account') as mocked_account: + mocked_storage = mock.MagicMock() + mocked_account.AccountFileStorage.return_value = mocked_storage + mocked_storage.find_all.return_value = ["an account"] + x = self._call_no_clientmock(["register", "--update-registration"]) + assert "can only change the e-mail" in x[0] + + def test_update_registration_with_email(self): + with mock.patch('certbot.main.client') as mocked_client: + with mock.patch('certbot.main.account') as mocked_account: + with mock.patch('certbot.main._determine_account') as mocked_det: + with mock.patch('certbot.main.client') as mocked_client: + mocked_storage = mock.MagicMock() + mocked_account.AccountFileStorage.return_value = mocked_storage + mocked_storage.find_all.return_value = ["an account"] + mocked_det.return_value = ("a", "b") + acme_client = mock.MagicMock() + mocked_client.Client.return_value = acme_client + # Currently the update_registration() call always + # raises a harmless acme_errors.UnexpectedUpdate. + # If this is fixed, we should get rid of both this + # side effect and the corresponding try/catch in + # main.register(). + uu = acme_errors.UnexpectedUpdate + acme_client.acme.update_registration.side_effect = uu + x = self._call_no_clientmock( + ["register", "--update-registration", "--email", + "user@example.org"]) + # When registration change succeeds, the return value + # of register() is None + assert x[0] is None + # and we got far enough to query the registration from + # the server + assert acme_client.acme.query_registration.call_count == 1 + class DetermineAccountTest(unittest.TestCase): """Tests for certbot.cli._determine_account.""" From fd899d21252b8bebd2729fbbebec216496c57902 Mon Sep 17 00:00:00 2001 From: Nick Le Mouton Date: Mon, 23 May 2016 09:44:06 +1200 Subject: [PATCH 170/396] Fixing package names for Debian Jessie --- docs/using.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/using.rst b/docs/using.rst index b10532259..f9af07613 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -460,7 +460,7 @@ repo, if you have not already done so. Then run: .. code-block:: shell - sudo apt-get install certbot python-certbot-apache -t jessie-backports + sudo apt-get install letsencrypt python-letsencrypt-apache -t jessie-backports **Fedora** From d4de026372780f079af3e6b811da991bad824ffe Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 23 May 2016 13:37:47 -0700 Subject: [PATCH 171/396] Add get_strict_version --- certbot/le_util.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/certbot/le_util.py b/certbot/le_util.py index f5148b949..1e5997d92 100644 --- a/certbot/le_util.py +++ b/certbot/le_util.py @@ -1,6 +1,9 @@ """Utilities for all Certbot.""" import argparse import collections +# distutils.version under virtualenv confuses pylint +# For more info, see: https://github.com/PyCQA/pylint/issues/73 +import distutils.version # pylint: disable=import-error,no-name-in-module import errno import logging import os @@ -342,3 +345,17 @@ def enforce_domain_sanity(domain): if not fqdn.match(domain): raise errors.ConfigurationError("Requested domain {0} is not a FQDN".format(domain)) return domain + + +def get_strict_version(normalized): + """Converts a normalized version to a strict version. + + :param str normalized: normalized version string + + :returns: An equivalent strict version + :rtype: distutils.version.StrictVersion + + """ + # strict version ending with "a" and a number designates a pre-release + # pylint: disable=no-member + return distutils.version.StrictVersion(normalized.replace(".dev", "a")) From 40a0354b360214485fbd19d90e24c9c9d040856c Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 23 May 2016 13:50:14 -0700 Subject: [PATCH 172/396] Start get_strict_version tests --- certbot/tests/le_util_test.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/certbot/tests/le_util_test.py b/certbot/tests/le_util_test.py index b6da4525f..db76615ee 100644 --- a/certbot/tests/le_util_test.py +++ b/certbot/tests/le_util_test.py @@ -339,5 +339,18 @@ class EnforceDomainSanityTest(unittest.TestCase): u"eichh\u00f6rnchen.example.com") +class GetStrictVersionTest(unittest.TestCase): + """Tests for certbot.le_util.get_strict_version.""" + + @classmethod + def _call(cls, *args, **kwargs): + from certbot.le_util import get_strict_version + return get_strict_version(*args, **kwargs) + + def test_two_dev_versions(self): + self.assertTrue( + self._call("0.0.0.dev20151006") < self._call("0.0.0.dev20151008")) + + if __name__ == "__main__": unittest.main() # pragma: no cover From 4caba1fc75749eb6c4f45dd61e314f62e26b962b Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 23 May 2016 13:53:09 -0700 Subject: [PATCH 173/396] Test mixed versions --- certbot/tests/le_util_test.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/certbot/tests/le_util_test.py b/certbot/tests/le_util_test.py index db76615ee..2dcecae2c 100644 --- a/certbot/tests/le_util_test.py +++ b/certbot/tests/le_util_test.py @@ -351,6 +351,10 @@ class GetStrictVersionTest(unittest.TestCase): self.assertTrue( self._call("0.0.0.dev20151006") < self._call("0.0.0.dev20151008")) + def test_one_dev_one_release_version(self): + self.assertTrue(self._call("1.0.0.dev0") < self._call("1.0.0")) + self.assertTrue(self._call("1.0.0") < self._call("1.0.1.dev0")) + if __name__ == "__main__": unittest.main() # pragma: no cover From ac37b9de6fdd5f935df4a5f83e0840f88df24d29 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 23 May 2016 13:55:17 -0700 Subject: [PATCH 174/396] test release version comparison --- certbot/tests/le_util_test.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/certbot/tests/le_util_test.py b/certbot/tests/le_util_test.py index 2dcecae2c..5bf93a406 100644 --- a/certbot/tests/le_util_test.py +++ b/certbot/tests/le_util_test.py @@ -355,6 +355,11 @@ class GetStrictVersionTest(unittest.TestCase): self.assertTrue(self._call("1.0.0.dev0") < self._call("1.0.0")) self.assertTrue(self._call("1.0.0") < self._call("1.0.1.dev0")) + def test_two_release_versions(self): + self.assertTrue(self._call("0.0.0") < self._call("0.0.1")) + self.assertTrue(self._call("0.0.0") < self._call("0.1.0")) + self.assertTrue(self._call("0.0.0") < self._call("1.0.0")) + if __name__ == "__main__": unittest.main() # pragma: no cover From 536234c5595347a472060beccdd9fd2e83791483 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 23 May 2016 13:59:16 -0700 Subject: [PATCH 175/396] try and catch problems if we do something silly with our version in the future --- certbot/tests/le_util_test.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/certbot/tests/le_util_test.py b/certbot/tests/le_util_test.py index 5bf93a406..6e4eef0f1 100644 --- a/certbot/tests/le_util_test.py +++ b/certbot/tests/le_util_test.py @@ -10,6 +10,7 @@ import unittest import mock import six +import certbot from certbot import errors @@ -360,6 +361,11 @@ class GetStrictVersionTest(unittest.TestCase): self.assertTrue(self._call("0.0.0") < self._call("0.1.0")) self.assertTrue(self._call("0.0.0") < self._call("1.0.0")) + def test_current_version(self): + current_version = self._call(certbot.__version__) + self.assertTrue(self._call("0.6.0") < current_version) + self.assertTrue(current_version < self._call("99.99.99")) + if __name__ == "__main__": unittest.main() # pragma: no cover From 0e9aec20a76d06895173ef815962447d75bb87d8 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 23 May 2016 14:04:13 -0700 Subject: [PATCH 176/396] Add CURRENT_VERSION constant --- certbot/storage.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/certbot/storage.py b/certbot/storage.py index c4bfb3e28..e2b26d322 100644 --- a/certbot/storage.py +++ b/certbot/storage.py @@ -8,6 +8,7 @@ import configobj import parsedatetime import pytz +import certbot from certbot import constants from certbot import crypto_util from certbot import errors @@ -17,6 +18,7 @@ from certbot import le_util logger = logging.getLogger(__name__) ALL_FOUR = ("cert", "privkey", "chain", "fullchain") +CURRENT_VERSION = le_util.get_strict_version(certbot.__version__) def config_with_defaults(config=None): From c3c9441a599226efe261d140d547977eb6ac8d71 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 23 May 2016 14:09:31 -0700 Subject: [PATCH 177/396] Save version in renewal config file --- certbot/storage.py | 1 + 1 file changed, 1 insertion(+) diff --git a/certbot/storage.py b/certbot/storage.py index e2b26d322..d4a469ca1 100644 --- a/certbot/storage.py +++ b/certbot/storage.py @@ -65,6 +65,7 @@ def write_renewal_config(o_filename, n_filename, target, relevant_data): """ config = configobj.ConfigObj(o_filename) + config["version"] = certbot.__version__ for kind in ALL_FOUR: config[kind] = target[kind] From 3576d372a67882a584c207b7bf720c10fc8d69c6 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 23 May 2016 14:32:43 -0700 Subject: [PATCH 178/396] Add warning about parsing old configuration file --- certbot/storage.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/certbot/storage.py b/certbot/storage.py index d4a469ca1..6c13eb844 100644 --- a/certbot/storage.py +++ b/certbot/storage.py @@ -262,6 +262,14 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes "renewal config file {0} is missing a required " "file reference".format(self.configfile)) + conf_version = self.configuration.get("version") + if (conf_version is not None and + le_util.get_strict_version(conf_version) > CURRENT_VERSION): + logger.warning( + "Attempting to parse the version %s renewal configuration " + "file found at %s with version %s of Certbot. This might not " + "work.", conf_version, config_filename, certbot.__version__) + self.cert = self.configuration["cert"] self.privkey = self.configuration["privkey"] self.chain = self.configuration["chain"] From ea53a14b57910bf9749d9304a3e93e02bea36d02 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 23 May 2016 14:58:14 -0700 Subject: [PATCH 179/396] test version is stored --- certbot/tests/storage_test.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/certbot/tests/storage_test.py b/certbot/tests/storage_test.py index be626edc5..aeba5c3ae 100644 --- a/certbot/tests/storage_test.py +++ b/certbot/tests/storage_test.py @@ -10,6 +10,7 @@ import configobj import mock import pytz +import certbot from certbot import configuration from certbot import errors from certbot.storage import ALL_FOUR @@ -760,11 +761,14 @@ class RenewableCertTests(BaseRenewableCertTest): with open(temp2, "r") as f: content = f.read() # useful value was updated - assert "useful = new_value" in content + self.assertTrue("useful = new_value" in content) # associated comment was preserved - assert "A useful value" in content + self.assertTrue("A useful value" in content) # useless value was deleted - assert "useless" not in content + self.assertTrue("useless" not in content) + # check version was stored + self.assertTrue("version = {0}".format(certbot.__version__) in content) + if __name__ == "__main__": unittest.main() # pragma: no cover From 15ddf2ea32a4f020b655c7ac4ca6d62cb0cafd50 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 23 May 2016 15:16:43 -0700 Subject: [PATCH 180/396] Test init with newer conf file --- certbot/tests/storage_test.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/certbot/tests/storage_test.py b/certbot/tests/storage_test.py index aeba5c3ae..b1444f311 100644 --- a/certbot/tests/storage_test.py +++ b/certbot/tests/storage_test.py @@ -138,6 +138,18 @@ class RenewableCertTests(BaseRenewableCertTest): self.assertRaises(errors.CertStorageError, storage.RenewableCert, config.filename, self.cli_config) + def test_renewal_newer_version(self): + from certbot import storage + + self._write_out_ex_kinds() + self.config["version"] = "99.99.99" + self.config.write() + + with mock.patch("certbot.storage.logger") as mock_logger: + storage.RenewableCert(self.config.filename, self.cli_config) + self.assertTrue(mock_logger.warning.called) + self.assertTrue("version" in mock_logger.warning.call_args[0][0]) + def test_consistent(self): # pylint: disable=too-many-statements,protected-access oldcert = self.test_rc.cert From 4919814dd17bdb8a7b0100ac89a5196cb4766310 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 23 May 2016 15:39:00 -0700 Subject: [PATCH 181/396] Pin old pkginfo version --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 4ee56576b..59da23de4 100644 --- a/setup.py +++ b/setup.py @@ -71,6 +71,7 @@ dev_extras = [ 'nose', 'nosexcover', 'pep8', + 'pkginfo<=1.2.1', 'pylint==1.4.2', # upstream #248 'tox', 'twine', From 2cfcfd6988dc84cf118004f860d926eb024cf1d6 Mon Sep 17 00:00:00 2001 From: Jacob Hoffman-Andrews Date: Mon, 23 May 2016 13:18:02 -0700 Subject: [PATCH 182/396] Run Boulder via docker-compose in tests. This removes a lot of setup code we used to need in order to get Boulder to run, and should reduce brittleness of tests based on Boulder changes. This also unblocks Boulder from upgrading to MariaDB 10.1 in integration tests, since changing to 10.1 syntax for user creation would break the current certbot integration tests (which run 10.0). --- .travis.yml | 25 ++++---------- tests/boulder-fetch.sh | 40 +++-------------------- tests/letstest/scripts/boulder_install.sh | 29 +++------------- tests/travis-integration.sh | 15 +++------ 4 files changed, 22 insertions(+), 87 deletions(-) diff --git a/.travis.yml b/.travis.yml index 16b5700e5..172f4c659 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,26 +4,18 @@ cache: directories: - $HOME/.cache/pip -services: - - rabbitmq - - mariadb - # apacheconftest - #- apache2 +# This makes sure we get a host with docker-compose present. +dist: trusty -# http://docs.travis-ci.com/user/ci-environment/#CI-environment-OS -# gimme has to be kept in sync with Boulder's Go version setting in .travis.yml before_install: - 'dpkg -s libaugeas0' - - '[ "xxx$BOULDER_INTEGRATION" = "xxx" ] || eval "$(gimme 1.5.1)"' # using separate envs with different TOXENVs creates 4x1 Travis build # matrix, which allows us to clearly distinguish which component under # test has failed env: global: - - GOPATH=/tmp/go - - PATH=$GOPATH/bin:$PATH - - GO15VENDOREXPERIMENT=1 # Fixes problems with vendor directories + - BOULDERPATH=$GOPATH/src/github.com/letsencrypt/boulder/ matrix: include: @@ -93,7 +85,6 @@ addons: - boulder - boulder-mysql - boulder-rabbitmq - mariadb: "10.0" apt: sources: - augeas @@ -109,13 +100,11 @@ addons: # For certbot-nginx integration testing - nginx-light - openssl - # For Boulder integration testing - - rsyslog # for apacheconftest - #- apache2 - #- libapache2-mod-wsgi - #- libapache2-mod-macro - #- sudo + - apache2 + - libapache2-mod-wsgi + - libapache2-mod-macro + - sudo install: "travis_retry pip install tox coveralls" script: 'travis_retry tox && ([ "xxx$BOULDER_INTEGRATION" = "xxx" ] || ./tests/travis-integration.sh)' diff --git a/tests/boulder-fetch.sh b/tests/boulder-fetch.sh index 0d8a3de38..11e36835a 100755 --- a/tests/boulder-fetch.sh +++ b/tests/boulder-fetch.sh @@ -1,40 +1,10 @@ #!/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 -go run cmd/rabbitmq-setup/main.go -server amqp://localhost -# listenbuddy is needed for ./start.py -go get github.com/jsha/listenbuddy -cd - +# Check out special branch until latest docker changes land in Boulder master. +git clone -b rev-rev https://github.com/letsencrypt/boulder $BOULDERPATH +cd $BOULDERPATH +sed -i 's/FAKE_DNS: .*/FAKE_DNS: 172.17.42.1/' docker-compose.yml +docker-compose up -d diff --git a/tests/letstest/scripts/boulder_install.sh b/tests/letstest/scripts/boulder_install.sh index b5ddf2c5b..936a64802 100755 --- a/tests/letstest/scripts/boulder_install.sh +++ b/tests/letstest/scripts/boulder_install.sh @@ -2,27 +2,8 @@ # >>>> only tested on Ubuntu 14.04LTS <<<< -# non-interactive install of mariadb and other dependencies -export DEBIAN_FRONTEND=noninteractive -sudo debconf-set-selections <<< 'mariadb-server mysql-server/root_password password PASS' -sudo debconf-set-selections <<< 'mariadb-server mysql-server/root_password_again password PASS' -apt-get -y --no-upgrade install git make libltdl3-dev mariadb-server rabbitmq-server -sudo mysql -uroot -pPASS -e "SET PASSWORD = PASSWORD(\'\');" - -# install go -wget https://storage.googleapis.com/golang/go1.5.1.linux-amd64.tar.gz -tar xzvf go1.5.1.linux-amd64.tar.gz -mkdir gocode -echo "export GOROOT=/home/ubuntu/go \n\ - export GOPATH=/home/ubuntu/gocode\n\ - export PATH=/home/ubuntu/go/bin:/home/ubuntu/gocode/bin:$PATH" >> .bashrc - -# install boulder and its go dependencies -go get -d github.com/letsencrypt/boulder/... -cd $GOPATH/src/github.com/letsencrypt/boulder -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 -go get github.com/jsha/listenbuddy +# Check out special branch until latest docker changes land in Boulder master. +git clone -b rev-rev https://github.com/letsencrypt/boulder $BOULDERPATH +cd $BOULDERPATH +sed -i 's/FAKE_DNS: .*/FAKE_DNS: 172.17.42.1/' docker-compose.yml +docker-compose up -d diff --git a/tests/travis-integration.sh b/tests/travis-integration.sh index 159a2ef80..b42617400 100755 --- a/tests/travis-integration.sh +++ b/tests/travis-integration.sh @@ -6,14 +6,9 @@ set -o errexit source .tox/$TOXENV/bin/activate -export CERTBOT_PATH=`pwd` +until curl http://boulder:4000/directory 2>/dev/null; do + echo waiting for boulder + sleep 1 +done -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 certbot -# boulder-interation.sh on its own. The --certbot flag says to run only the -# certbot 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 certbot tests, but that will be better built off of this. -python test/integration-test.py --certbot +./tests/boulder-integration.sh From ed802a664813ff12d4da4874b1591a90c8391df4 Mon Sep 17 00:00:00 2001 From: Jacob Hoffman-Andrews Date: Mon, 23 May 2016 18:47:52 -0700 Subject: [PATCH 183/396] Fix BOULDERPATH --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 172f4c659..6f964dbec 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,7 +15,7 @@ before_install: # test has failed env: global: - - BOULDERPATH=$GOPATH/src/github.com/letsencrypt/boulder/ + - BOULDERPATH=$PWD/boulder/ matrix: include: From b54497d8145e0999ac3f163ae38e3bc70a7a5549 Mon Sep 17 00:00:00 2001 From: sagi Date: Tue, 24 May 2016 19:33:13 +0000 Subject: [PATCH 184/396] Fix chain filename --- tests/boulder-integration.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/boulder-integration.sh b/tests/boulder-integration.sh index a1245e1c9..323ea004b 100755 --- a/tests/boulder-integration.sh +++ b/tests/boulder-integration.sh @@ -44,7 +44,7 @@ common auth --csr "$CSR_PATH" \ --cert-path "${root}/csr/cert.pem" \ --chain-path "${root}/csr/chain.pem" openssl x509 -in "${root}/csr/cert.pem" -text -openssl x509 -in "${root}/csr/0000_chain.pem" -text +openssl x509 -in "${root}/csr/chain.pem" -text common --domains le3.wtf install \ --cert-path "${root}/csr/cert.pem" \ From b1eff0fe3528f0cef2a077b6dcee777d1dbebb8c Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Tue, 24 May 2016 13:03:53 -0700 Subject: [PATCH 185/396] Build le-auto to bring it up to date --- letsencrypt-auto-source/letsencrypt-auto | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/letsencrypt-auto-source/letsencrypt-auto b/letsencrypt-auto-source/letsencrypt-auto index eb5561070..ea085454c 100755 --- a/letsencrypt-auto-source/letsencrypt-auto +++ b/letsencrypt-auto-source/letsencrypt-auto @@ -425,7 +425,8 @@ BootstrapMac() { $pkgcmd augeas $pkgcmd dialog - if [ "$(which python)" = "/System/Library/Frameworks/Python.framework/Versions/2.7/bin/python" ]; then + if [ "$(which python)" = "/System/Library/Frameworks/Python.framework/Versions/2.7/bin/python" \ + -o "$(which python)" = "/usr/bin/python" ]; then # We want to avoid using the system Python because it requires root to use pip. # python.org, MacPorts or HomeBrew Python installations should all be OK. echo "Installing python..." From 70bb7ff68f2a9eb5fac7b6cc494a50dce99ade20 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Tue, 24 May 2016 13:08:10 -0700 Subject: [PATCH 186/396] fixes #3060 --- letsencrypt-auto-source/letsencrypt-auto | 3 +-- letsencrypt-auto-source/letsencrypt-auto.template | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/letsencrypt-auto-source/letsencrypt-auto b/letsencrypt-auto-source/letsencrypt-auto index ea085454c..2de4b053e 100755 --- a/letsencrypt-auto-source/letsencrypt-auto +++ b/letsencrypt-auto-source/letsencrypt-auto @@ -935,6 +935,7 @@ else if [ "$NO_SELF_UPGRADE" != 1 ]; then TEMP_DIR=$(TempDir) + trap 'rm -rf "$TEMP_DIR"' EXIT # --------------------------------------------------------------------------- cat << "UNLIKELY_EOF" > "$TEMP_DIR/fetch.py" """Do downloading and JSON parsing without additional dependencies. :: @@ -1089,8 +1090,6 @@ UNLIKELY_EOF # filesystems is non-atomic, doing `rm dest, cp src dest, rm src`, but the # cp is unlikely to fail (esp. under sudo) if the rm doesn't. $SUDO mv -f "$TEMP_DIR/letsencrypt-auto.permission-clone" "$0" - # TODO: Clean up temp dir safely, even if it has quotes in its path. - rm -rf "$TEMP_DIR" fi # A newer version is available. fi # Self-upgrading is allowed. diff --git a/letsencrypt-auto-source/letsencrypt-auto.template b/letsencrypt-auto-source/letsencrypt-auto.template index f1ed82c4c..116894a93 100755 --- a/letsencrypt-auto-source/letsencrypt-auto.template +++ b/letsencrypt-auto-source/letsencrypt-auto.template @@ -291,6 +291,7 @@ else if [ "$NO_SELF_UPGRADE" != 1 ]; then TEMP_DIR=$(TempDir) + trap 'rm -rf "$TEMP_DIR"' EXIT # --------------------------------------------------------------------------- cat << "UNLIKELY_EOF" > "$TEMP_DIR/fetch.py" {{ fetch.py }} @@ -319,8 +320,6 @@ UNLIKELY_EOF # filesystems is non-atomic, doing `rm dest, cp src dest, rm src`, but the # cp is unlikely to fail (esp. under sudo) if the rm doesn't. $SUDO mv -f "$TEMP_DIR/letsencrypt-auto.permission-clone" "$0" - # TODO: Clean up temp dir safely, even if it has quotes in its path. - rm -rf "$TEMP_DIR" fi # A newer version is available. fi # Self-upgrading is allowed. From c606273d1489a27c50376fca6244968a4ccde06a Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Tue, 24 May 2016 13:16:21 -0700 Subject: [PATCH 187/396] use TEMP_DIR trap consistently --- letsencrypt-auto-source/letsencrypt-auto | 2 +- letsencrypt-auto-source/letsencrypt-auto.template | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/letsencrypt-auto-source/letsencrypt-auto b/letsencrypt-auto-source/letsencrypt-auto index 2de4b053e..b65c29a44 100755 --- a/letsencrypt-auto-source/letsencrypt-auto +++ b/letsencrypt-auto-source/letsencrypt-auto @@ -532,6 +532,7 @@ if [ "$1" = "--le-auto-phase2" ]; then echo "Installing Python packages..." TEMP_DIR=$(TempDir) + trap 'rm -rf "$TEMP_DIR"' EXIT # There is no $ interpolation due to quotes on starting heredoc delimiter. # ------------------------------------------------------------------------- cat << "UNLIKELY_EOF" > "$TEMP_DIR/letsencrypt-auto-requirements.txt" @@ -889,7 +890,6 @@ UNLIKELY_EOF PIP_OUT=`"$VENV_BIN/pip" install --no-cache-dir --require-hashes -r "$TEMP_DIR/letsencrypt-auto-requirements.txt" 2>&1` PIP_STATUS=$? set -e - rm -rf "$TEMP_DIR" if [ "$PIP_STATUS" != 0 ]; then # Report error. (Otherwise, be quiet.) echo "Had a problem while installing Python packages:" diff --git a/letsencrypt-auto-source/letsencrypt-auto.template b/letsencrypt-auto-source/letsencrypt-auto.template index 116894a93..43d8bc7e1 100755 --- a/letsencrypt-auto-source/letsencrypt-auto.template +++ b/letsencrypt-auto-source/letsencrypt-auto.template @@ -229,6 +229,7 @@ if [ "$1" = "--le-auto-phase2" ]; then echo "Installing Python packages..." TEMP_DIR=$(TempDir) + trap 'rm -rf "$TEMP_DIR"' EXIT # There is no $ interpolation due to quotes on starting heredoc delimiter. # ------------------------------------------------------------------------- cat << "UNLIKELY_EOF" > "$TEMP_DIR/letsencrypt-auto-requirements.txt" @@ -245,7 +246,6 @@ UNLIKELY_EOF PIP_OUT=`"$VENV_BIN/pip" install --no-cache-dir --require-hashes -r "$TEMP_DIR/letsencrypt-auto-requirements.txt" 2>&1` PIP_STATUS=$? set -e - rm -rf "$TEMP_DIR" if [ "$PIP_STATUS" != 0 ]; then # Report error. (Otherwise, be quiet.) echo "Had a problem while installing Python packages:" From 420e64f03951bbce7737d0b0ce05fad9070763db Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Tue, 24 May 2016 13:43:45 -0700 Subject: [PATCH 188/396] Simplify webroot chown and rm errors --- certbot/plugins/webroot.py | 22 +++++---------------- certbot/plugins/webroot_test.py | 35 ++------------------------------- 2 files changed, 7 insertions(+), 50 deletions(-) diff --git a/certbot/plugins/webroot.py b/certbot/plugins/webroot.py index c344954ae..508000d77 100644 --- a/certbot/plugins/webroot.py +++ b/certbot/plugins/webroot.py @@ -181,12 +181,8 @@ to serve all files under specified web root ({0}).""" os.chown(self.full_roots[name], stat_path.st_uid, stat_path.st_gid) except OSError as exception: - if exception.errno == errno.EACCES: - logger.debug("Insufficient permissions to change owner and uid - ignoring") - else: - raise errors.PluginError( - "Couldn't create root for {0} http-01 " - "challenge responses: {1}", name, exception) + logger.debug("Unable to change owner and uid of webroot directory") + logger.debug("Error was: %s", exception) except OSError as exception: if exception.errno != errno.EEXIST: @@ -235,17 +231,9 @@ to serve all files under specified web root ({0}).""" logger.debug("All challenges cleaned up, removing %s", root_path) except OSError as exc: - if exc.errno == errno.ENOTEMPTY: - logger.debug("Challenges cleaned up but %s not empty", - root_path) - elif exc.errno == errno.EACCES: - logger.debug("Challenges cleaned up but no permissions for %s", - root_path) - elif exc.errno == errno.ENOENT: - logger.debug("Challenges cleaned up, %s does not exists", - root_path) - else: - raise + logger.debug( + "Unable to clean up challenge directory %s", root_path) + logger.debug("Error was: %s", exc) class _WebrootMapAction(argparse.Action): diff --git a/certbot/plugins/webroot_test.py b/certbot/plugins/webroot_test.py index ab2a9e9a4..5d784a75c 100644 --- a/certbot/plugins/webroot_test.py +++ b/certbot/plugins/webroot_test.py @@ -138,15 +138,10 @@ class AuthenticatorTest(unittest.TestCase): os.chmod(self.path, 0o700) @mock.patch("certbot.plugins.webroot.os.chown") - def test_failed_chown_eacces(self, mock_chown): + def test_failed_chown(self, mock_chown): mock_chown.side_effect = OSError(errno.EACCES, "msg") self.auth.perform([self.achall]) # exception caught and logged - @mock.patch("certbot.plugins.webroot.os.chown") - def test_failed_chown_not_eacces(self, mock_chown): - mock_chown.side_effect = OSError() - self.assertRaises(errors.PluginError, self.auth.perform, []) - def test_perform_permissions(self): self.auth.prepare() @@ -200,7 +195,7 @@ class AuthenticatorTest(unittest.TestCase): os.rmdir(leftover_path) @mock.patch('os.rmdir') - def test_cleanup_permission_denied(self, mock_rmdir): + def test_cleanup_failure(self, mock_rmdir): self.auth.prepare() self.auth.perform([self.achall]) @@ -212,32 +207,6 @@ class AuthenticatorTest(unittest.TestCase): self.assertFalse(os.path.exists(self.validation_path)) self.assertTrue(os.path.exists(self.root_challenge_path)) - @mock.patch('os.rmdir') - def test_cleanup_oserror(self, mock_rmdir): - self.auth.prepare() - self.auth.perform([self.achall]) - - os_error = OSError() - os_error.errno = errno.EPERM - mock_rmdir.side_effect = os_error - - self.assertRaises(OSError, self.auth.cleanup, [self.achall]) - self.assertFalse(os.path.exists(self.validation_path)) - self.assertTrue(os.path.exists(self.root_challenge_path)) - - @mock.patch('os.rmdir') - def test_cleanup_file_not_exists(self, mock_rmdir): - self.auth.prepare() - self.auth.perform([self.achall]) - - os_error = OSError() - os_error.errno = errno.ENOENT - mock_rmdir.side_effect = os_error - - self.auth.cleanup([self.achall]) - self.assertFalse(os.path.exists(self.validation_path)) - self.assertTrue(os.path.exists(self.root_challenge_path)) - class WebrootActionTest(unittest.TestCase): """Tests for webroot argparse actions.""" From c01e2c259af0e200222e66bdf27434f3dba47613 Mon Sep 17 00:00:00 2001 From: Jacob Hoffman-Andrews Date: Tue, 24 May 2016 15:38:03 -0700 Subject: [PATCH 189/396] Check out Boulder master instead of branch. --- tests/boulder-fetch.sh | 2 +- tests/letstest/scripts/boulder_install.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/boulder-fetch.sh b/tests/boulder-fetch.sh index 11e36835a..a09d0adf9 100755 --- a/tests/boulder-fetch.sh +++ b/tests/boulder-fetch.sh @@ -3,7 +3,7 @@ set -xe # Check out special branch until latest docker changes land in Boulder master. -git clone -b rev-rev https://github.com/letsencrypt/boulder $BOULDERPATH +git clone https://github.com/letsencrypt/boulder $BOULDERPATH cd $BOULDERPATH sed -i 's/FAKE_DNS: .*/FAKE_DNS: 172.17.42.1/' docker-compose.yml docker-compose up -d diff --git a/tests/letstest/scripts/boulder_install.sh b/tests/letstest/scripts/boulder_install.sh index 936a64802..426642880 100755 --- a/tests/letstest/scripts/boulder_install.sh +++ b/tests/letstest/scripts/boulder_install.sh @@ -3,7 +3,7 @@ # >>>> only tested on Ubuntu 14.04LTS <<<< # Check out special branch until latest docker changes land in Boulder master. -git clone -b rev-rev https://github.com/letsencrypt/boulder $BOULDERPATH +git clone https://github.com/letsencrypt/boulder $BOULDERPATH cd $BOULDERPATH sed -i 's/FAKE_DNS: .*/FAKE_DNS: 172.17.42.1/' docker-compose.yml docker-compose up -d From 57e6d1995bebf2a700f83e1dfaff2c626d6d8d18 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Tue, 24 May 2016 16:32:03 -0700 Subject: [PATCH 190/396] log louder --- certbot/plugins/webroot.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/certbot/plugins/webroot.py b/certbot/plugins/webroot.py index 508000d77..624ee2ff4 100644 --- a/certbot/plugins/webroot.py +++ b/certbot/plugins/webroot.py @@ -181,7 +181,7 @@ to serve all files under specified web root ({0}).""" os.chown(self.full_roots[name], stat_path.st_uid, stat_path.st_gid) except OSError as exception: - logger.debug("Unable to change owner and uid of webroot directory") + logger.info("Unable to change owner and uid of webroot directory") logger.debug("Error was: %s", exception) except OSError as exception: @@ -231,7 +231,7 @@ to serve all files under specified web root ({0}).""" logger.debug("All challenges cleaned up, removing %s", root_path) except OSError as exc: - logger.debug( + logger.info( "Unable to clean up challenge directory %s", root_path) logger.debug("Error was: %s", exc) From 87d0e938ad8912cd1b17057a3009f2044e46f5c7 Mon Sep 17 00:00:00 2001 From: Amjad Mashaal Date: Thu, 5 May 2016 09:09:41 +0200 Subject: [PATCH 191/396] Adding --dialog argument --- certbot/cli.py | 3 +++ certbot/main.py | 3 ++- certbot/tests/cli_test.py | 6 ++++++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/certbot/cli.py b/certbot/cli.py index e2c57595b..f96bf2656 100644 --- a/certbot/cli.py +++ b/certbot/cli.py @@ -569,6 +569,9 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): help="Run without ever asking for user input. This may require " "additional command line flags; the client will try to explain " "which ones are required if it finds one missing") + helpful.add( + None, "--dialog", dest="dialog_mode", action="store_true", + help="Run using dialog") helpful.add( None, "--dry-run", action="store_true", dest="dry_run", help="Perform a test run of the client, obtaining test (invalid) certs" diff --git a/certbot/main.py b/certbot/main.py index 309889e8e..161e7d7cd 100644 --- a/certbot/main.py +++ b/certbot/main.py @@ -677,8 +677,9 @@ def main(cli_args=sys.argv[1:]): displayer = display_util.NoninteractiveDisplay(sys.stdout) elif config.text_mode: displayer = display_util.FileDisplay(sys.stdout) + elif config.dialog_mode: + displayer = display_util.NcursesDisplay() elif config.verb == "renew": - config.noninteractive_mode = True displayer = display_util.NoninteractiveDisplay(sys.stdout) else: displayer = display_util.NcursesDisplay() diff --git a/certbot/tests/cli_test.py b/certbot/tests/cli_test.py index 31056cafe..ca33f4524 100644 --- a/certbot/tests/cli_test.py +++ b/certbot/tests/cli_test.py @@ -149,6 +149,12 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods args.extend(['--email', 'io@io.is']) self._cli_missing_flag(args, "--agree-tos") + @mock.patch('certbot.main.renew') + def test_gui(self, renew): + args = ['renew', '--dialog'] + self._call(args) + self.assertFalse(renew.call_args[0][0].noninteractive_mode) + @mock.patch('certbot.main.client.acme_client.Client') @mock.patch('certbot.main._determine_account') @mock.patch('certbot.main.client.Client.obtain_and_enroll_certificate') From 8772a9846f5c7e834dc23863e4905f5676159fc0 Mon Sep 17 00:00:00 2001 From: Amjad Mashaal Date: Wed, 25 May 2016 02:52:47 +0200 Subject: [PATCH 192/396] Raising error on conflicting args --- certbot/main.py | 10 ++++++++++ certbot/tests/cli_test.py | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/certbot/main.py b/certbot/main.py index 161e7d7cd..d900c65fa 100644 --- a/certbot/main.py +++ b/certbot/main.py @@ -669,6 +669,16 @@ def main(cli_args=sys.argv[1:]): sys.excepthook = functools.partial(_handle_exception, config=config) + # Avoid conflicting args + conficting_args = ["quiet", "noninteractive_mode", "text_mode"] + if config.dialog_mode: + for arg in conficting_args: + if getattr(config, arg): + raise errors.Error( + ("Conflicting values for displayer." + " {0} conflicts with dialog_mode").format(arg) + ) + # Displayer if config.quiet: config.noninteractive_mode = True diff --git a/certbot/tests/cli_test.py b/certbot/tests/cli_test.py index ca33f4524..64cf98c2c 100644 --- a/certbot/tests/cli_test.py +++ b/certbot/tests/cli_test.py @@ -49,7 +49,7 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods self.logs_dir = os.path.join(self.tmp_dir, 'logs') self.standard_args = ['--config-dir', self.config_dir, '--work-dir', self.work_dir, - '--logs-dir', self.logs_dir, '--text'] + '--logs-dir', self.logs_dir] def tearDown(self): shutil.rmtree(self.tmp_dir) From 1fd44d9302b5fa374dfc0c87416eb6006bccc8d2 Mon Sep 17 00:00:00 2001 From: Amjad Mashaal Date: Wed, 25 May 2016 03:55:52 +0200 Subject: [PATCH 193/396] Fixing tests --- certbot/cli.py | 3 +++ certbot/tests/cli_test.py | 4 +++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/certbot/cli.py b/certbot/cli.py index f96bf2656..e2878c1e9 100644 --- a/certbot/cli.py +++ b/certbot/cli.py @@ -552,6 +552,9 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): :rtype: argparse.Namespace """ + + # pylint: disable=too-many-statements + helpful = HelpfulArgumentParser(args, plugins, detect_defaults) # --help is automatically provided by argparse diff --git a/certbot/tests/cli_test.py b/certbot/tests/cli_test.py index 64cf98c2c..7e352febb 100644 --- a/certbot/tests/cli_test.py +++ b/certbot/tests/cli_test.py @@ -49,7 +49,7 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods self.logs_dir = os.path.join(self.tmp_dir, 'logs') self.standard_args = ['--config-dir', self.config_dir, '--work-dir', self.work_dir, - '--logs-dir', self.logs_dir] + '--logs-dir', self.logs_dir, '--text'] def tearDown(self): shutil.rmtree(self.tmp_dir) @@ -152,6 +152,8 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods @mock.patch('certbot.main.renew') def test_gui(self, renew): args = ['renew', '--dialog'] + # --text conflicts with --dialog + self.standard_args.remove('--text') self._call(args) self.assertFalse(renew.call_args[0][0].noninteractive_mode) From 353abaa608c74f2a331ee920515aa713d6dd1827 Mon Sep 17 00:00:00 2001 From: Amjad Mashaal Date: Wed, 25 May 2016 04:59:09 +0200 Subject: [PATCH 194/396] Adding args conflict test to main_test.py --- certbot/tests/main_test.py | 39 +++++++++++++++++++++++++++++++++++--- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/certbot/tests/main_test.py b/certbot/tests/main_test.py index 66cba64a3..e3f8b860d 100644 --- a/certbot/tests/main_test.py +++ b/certbot/tests/main_test.py @@ -1,15 +1,49 @@ """Tests for certbot.main.""" +import os +import shutil +import tempfile import unittest - import mock - +import six from certbot import cli from certbot import configuration +from certbot import errors +from certbot import main from certbot.plugins import disco as plugins_disco +class MainTest(unittest.TestCase): + """Tests for certbot.main""" + + def setUp(self): + self.tmp_dir = tempfile.mkdtemp() + self.config_dir = os.path.join(self.tmp_dir, 'config') + self.work_dir = os.path.join(self.tmp_dir, 'work') + self.logs_dir = os.path.join(self.tmp_dir, 'logs') + self.standard_args = ['--config-dir', self.config_dir, + '--work-dir', self.work_dir, + '--logs-dir', self.logs_dir, '--text'] + + def tearDown(self): + shutil.rmtree(self.tmp_dir) + + def _call(self, args, stdout=None): + "Run the client with output streams mocked out" + args = self.standard_args + args + + toy_stdout = stdout if stdout else six.StringIO() + with mock.patch('certbot.main.sys.stdout', new=toy_stdout): + with mock.patch('certbot.main.sys.stderr') as stderr: + ret = main.main(args[:]) # NOTE: parser can alter its args! + return ret, toy_stdout, stderr + + def test_args_conflict(self): + args = ['renew', '--dialog', '--text'] + self.assertRaises(errors.Error, self._call, args) + + class ObtainCertTest(unittest.TestCase): """Tests for certbot.main.obtain_cert.""" @@ -26,7 +60,6 @@ class ObtainCertTest(unittest.TestCase): config = configuration.NamespaceConfig( cli.prepare_and_parse_args(plugins, args)) - from certbot import main with mock.patch('certbot.main._init_le_client') as mock_init: main.obtain_cert(config, plugins) From e93aeb88dd4da8e26fd6a4c257234ba990aae403 Mon Sep 17 00:00:00 2001 From: sagi Date: Wed, 25 May 2016 04:19:18 +0000 Subject: [PATCH 195/396] Fix docs --- certbot/client.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/certbot/client.py b/certbot/client.py index 514da879e..9ce326822 100644 --- a/certbot/client.py +++ b/certbot/client.py @@ -580,7 +580,8 @@ def _open_pem_file(cli_arg_path, pem_path): :param str cli_arg_path: the cli arg name, e.g. cert_path :param str pem_path: the pem file path to open - :returns a file object + :returns: a tuple of file object and its absolute file path + """ if cli.set_by_cli(cli_arg_path): return le_util.safe_open(pem_path, chmod=0o644),\ From 13e165e2f9ef3af630d6dbe74082aa689f35be31 Mon Sep 17 00:00:00 2001 From: Noah Swartz Date: Tue, 24 May 2016 21:30:45 -0700 Subject: [PATCH 196/396] add exception type --- certbot/tests/reverter_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certbot/tests/reverter_test.py b/certbot/tests/reverter_test.py index c79c72a48..85234b76a 100644 --- a/certbot/tests/reverter_test.py +++ b/certbot/tests/reverter_test.py @@ -39,7 +39,7 @@ class ReverterCheckpointLocalTest(unittest.TestCase): mock_read.side_effect = OSError("cannot even") try: self.reverter.add_to_checkpoint(self.sets[0], "save1") - except: + except OSError: pass self.reverter.finalize_checkpoint("blah") path = os.listdir(self.reverter.config.backup_dir)[0] From 4a85d59d9353e7a0567c0bc56c9729d75b96d209 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Tue, 24 May 2016 22:03:30 -0700 Subject: [PATCH 197/396] Add test for missing renewal conf version --- certbot/tests/storage_test.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/certbot/tests/storage_test.py b/certbot/tests/storage_test.py index b1444f311..f19b7d89d 100644 --- a/certbot/tests/storage_test.py +++ b/certbot/tests/storage_test.py @@ -138,6 +138,16 @@ class RenewableCertTests(BaseRenewableCertTest): self.assertRaises(errors.CertStorageError, storage.RenewableCert, config.filename, self.cli_config) + def test_no_renewal_version(self): + from certbot import storage + + self._write_out_ex_kinds() + self.assertTrue("version" not in self.config) + + with mock.patch("certbot.storage.logger") as mock_logger: + storage.RenewableCert(self.config.filename, self.cli_config) + self.assertFalse(mock_logger.warning.called) + def test_renewal_newer_version(self): from certbot import storage From b158d03e9754e8674903fa7bc3ea00642e5c361c Mon Sep 17 00:00:00 2001 From: Amjad Mashaal Date: Wed, 25 May 2016 16:30:29 +0200 Subject: [PATCH 198/396] Revert removal of a needed line --- certbot/main.py | 1 + 1 file changed, 1 insertion(+) diff --git a/certbot/main.py b/certbot/main.py index d900c65fa..0f2aece1e 100644 --- a/certbot/main.py +++ b/certbot/main.py @@ -690,6 +690,7 @@ def main(cli_args=sys.argv[1:]): elif config.dialog_mode: displayer = display_util.NcursesDisplay() elif config.verb == "renew": + config.noninteractive_mode = True displayer = display_util.NoninteractiveDisplay(sys.stdout) else: displayer = display_util.NcursesDisplay() From d1df72d63c7eb9e590d05651f0f1a41caf234a1d Mon Sep 17 00:00:00 2001 From: sagi Date: Wed, 25 May 2016 19:06:25 +0000 Subject: [PATCH 199/396] Add the chain_cert is None case --- certbot/client.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/certbot/client.py b/certbot/client.py index 9ce326822..ba31f8760 100644 --- a/certbot/client.py +++ b/certbot/client.py @@ -328,7 +328,9 @@ class Client(object): logger.info("Server issued certificate; certificate written to %s", abs_cert_path) - if chain_cert: + if not chain_cert: + return abs_cert_path, None, None + else: chain_pem = crypto_util.dump_pyopenssl_chain(chain_cert) chain_file, abs_chain_path =\ @@ -339,7 +341,7 @@ class Client(object): _save_chain(chain_pem, chain_file) _save_chain(cert_pem + chain_pem, fullchain_file) - return abs_cert_path, abs_chain_path, abs_fullchain_path + return abs_cert_path, abs_chain_path, abs_fullchain_path def deploy_certificate(self, domains, privkey_path, cert_path, chain_path, fullchain_path): From b3aeeefe20aae784fb6959e935dde87a3c761feb Mon Sep 17 00:00:00 2001 From: sagi Date: Wed, 25 May 2016 20:03:45 +0000 Subject: [PATCH 200/396] Autoconfigure OCSP Stapling with --must-staple --- certbot/client.py | 5 +++-- certbot/interfaces.py | 6 +++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/certbot/client.py b/certbot/client.py index 0159d3946..a81b7cd70 100644 --- a/certbot/client.py +++ b/certbot/client.py @@ -398,6 +398,7 @@ class Client(object): hsts = config.hsts if "ensure-http-header" in supported else False uir = config.uir if "ensure-http-header" in supported else False staple = config.staple if "staple-ocsp" in supported else False + must_staple = config.must_staple if redirect is None: redirect = enhancements.ask("redirect") @@ -411,11 +412,11 @@ class Client(object): if uir: self.apply_enhancement(domains, "ensure-http-header", "Upgrade-Insecure-Requests") - if staple: + if staple or must_staple: self.apply_enhancement(domains, "staple-ocsp") msg = ("We were unable to restart web server") - if redirect or hsts or uir or staple: + if redirect or hsts or uir or staple or must_staple: with error_handler.ErrorHandler(self._rollback_and_restart, msg): self.installer.restart() diff --git a/certbot/interfaces.py b/certbot/interfaces.py index 8e8666e70..19d9f0c07 100644 --- a/certbot/interfaces.py +++ b/certbot/interfaces.py @@ -201,9 +201,9 @@ class IConfig(zope.interface.Interface): "Email used for registration and recovery contact.") rsa_key_size = zope.interface.Attribute("Size of the RSA key.") must_staple = zope.interface.Attribute( - "Whether to request the OCSP Must Staple certificate extension. " - "Additional setup may be required after issuance. This does not " - "currently autoconfigure web servers for OCSP stapling. ") + "Adds the OCSP Must Staple extension to the certificate." + "Autoconfigures OCSP Stapling for supported setups " + "(Apache version >= 2.3.3 ).") config_dir = zope.interface.Attribute("Configuration directory.") work_dir = zope.interface.Attribute("Working directory.") From 0999705691ae6040ae6dd7221c6069dd12cfd9f0 Mon Sep 17 00:00:00 2001 From: Amjad Mashaal Date: Wed, 25 May 2016 02:15:50 +0200 Subject: [PATCH 201/396] Fixing wrapping for messages with URLs --- certbot/display/util.py | 23 ++++++++++++++++++----- certbot/reporter.py | 9 +++++++-- 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/certbot/display/util.py b/certbot/display/util.py index 8de607534..b4004997f 100644 --- a/certbot/display/util.py +++ b/certbot/display/util.py @@ -41,10 +41,15 @@ def _wrap_lines(msg): """ lines = msg.splitlines() fixed_l = [] - for line in lines: - fixed_l.append(textwrap.fill(line, 80)) - return os.linesep.join(fixed_l) + for line in lines: + fixed_l.append(textwrap.fill( + line, + 80, + break_long_words=False, + break_on_hyphens=False)) + + return os.linesep.join(fixed_l) @zope.interface.implementer(interfaces.IDisplay) class NcursesDisplay(object): @@ -265,7 +270,11 @@ class FileDisplay(object): """ ans = raw_input( - textwrap.fill("%s (Enter 'c' to cancel): " % message, 80)) + textwrap.fill( + "%s (Enter 'c' to cancel): " % message, + 80, + break_long_words=False, + break_on_hyphens=False)) if ans == "c" or ans == "C": return CANCEL, "-1" @@ -402,7 +411,11 @@ class FileDisplay(object): # Write out the menu choices for i, desc in enumerate(choices, 1): self.outfile.write( - textwrap.fill("{num}: {desc}".format(num=i, desc=desc), 80)) + textwrap.fill( + "{num}: {desc}".format(num=i, desc=desc), + 80, + break_long_words=False, + break_on_hyphens=False)) # Keep this outside of the textwrap self.outfile.write(os.linesep) diff --git a/certbot/reporter.py b/certbot/reporter.py index d509cb0b8..43e8cd0dc 100644 --- a/certbot/reporter.py +++ b/certbot/reporter.py @@ -82,10 +82,15 @@ class Reporter(object): print(le_util.ANSI_SGR_BOLD) print('IMPORTANT NOTES:') first_wrapper = textwrap.TextWrapper( - initial_indent=' - ', subsequent_indent=(' ' * 3)) + initial_indent=' - ', + subsequent_indent=(' ' * 3), + break_long_words=False, + break_on_hyphens=False) next_wrapper = textwrap.TextWrapper( initial_indent=first_wrapper.subsequent_indent, - subsequent_indent=first_wrapper.subsequent_indent) + subsequent_indent=first_wrapper.subsequent_indent, + break_long_words=False, + break_on_hyphens=False) while not self.messages.empty(): msg = self.messages.get() if self.config.quiet: From 5a3397cf63ac0a4d0d5196e6bf20c50ec3356f96 Mon Sep 17 00:00:00 2001 From: sagi Date: Wed, 25 May 2016 21:07:47 +0000 Subject: [PATCH 202/396] Fix tests --- certbot/tests/client_test.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/certbot/tests/client_test.py b/certbot/tests/client_test.py index 8490efd9f..e1aea1b64 100644 --- a/certbot/tests/client_test.py +++ b/certbot/tests/client_test.py @@ -306,7 +306,7 @@ class ClientTest(unittest.TestCase): @mock.patch("certbot.client.enhancements") def test_enhance_config(self, mock_enhancements): - config = ConfigHelper(redirect=True, hsts=False, uir=False) + config = ConfigHelper(redirect=True, hsts=False, uir=False, must_staple=False) self.assertRaises(errors.Error, self.client.enhance_config, ["foo.bar"], config) @@ -322,7 +322,7 @@ class ClientTest(unittest.TestCase): @mock.patch("certbot.client.enhancements") def test_enhance_config_no_ask(self, mock_enhancements): - config = ConfigHelper(redirect=True, hsts=False, uir=False) + config = ConfigHelper(redirect=True, hsts=False, uir=False, must_staple=False) self.assertRaises(errors.Error, self.client.enhance_config, ["foo.bar"], config) @@ -331,16 +331,16 @@ class ClientTest(unittest.TestCase): self.client.installer = installer installer.supported_enhancements.return_value = ["redirect", "ensure-http-header"] - config = ConfigHelper(redirect=True, hsts=False, uir=False) + config = ConfigHelper(redirect=True, hsts=False, uir=False, must_staple=False) self.client.enhance_config(["foo.bar"], config) installer.enhance.assert_called_with("foo.bar", "redirect", None) - config = ConfigHelper(redirect=False, hsts=True, uir=False) + config = ConfigHelper(redirect=False, hsts=True, uir=False, must_staple=False) self.client.enhance_config(["foo.bar"], config) installer.enhance.assert_called_with("foo.bar", "ensure-http-header", "Strict-Transport-Security") - config = ConfigHelper(redirect=False, hsts=False, uir=True) + config = ConfigHelper(redirect=False, hsts=False, uir=True, must_staple=False) self.client.enhance_config(["foo.bar"], config) installer.enhance.assert_called_with("foo.bar", "ensure-http-header", "Upgrade-Insecure-Requests") @@ -354,13 +354,13 @@ class ClientTest(unittest.TestCase): self.client.installer = installer installer.supported_enhancements.return_value = [] - config = ConfigHelper(redirect=None, hsts=True, uir=True) + config = ConfigHelper(redirect=None, hsts=True, uir=True, must_staple=False) self.client.enhance_config(["foo.bar"], config) installer.enhance.assert_not_called() mock_enhancements.ask.assert_not_called() def test_enhance_config_no_installer(self): - config = ConfigHelper(redirect=True, hsts=False, uir=False) + config = ConfigHelper(redirect=True, hsts=False, uir=False, must_staple=False) self.assertRaises(errors.Error, self.client.enhance_config, ["foo.bar"], config) @@ -374,7 +374,7 @@ class ClientTest(unittest.TestCase): installer.supported_enhancements.return_value = ["redirect"] installer.enhance.side_effect = errors.PluginError - config = ConfigHelper(redirect=True, hsts=False, uir=False) + config = ConfigHelper(redirect=True, hsts=False, uir=False, must_staple=False) self.assertRaises(errors.PluginError, self.client.enhance_config, ["foo.bar"], config) @@ -391,7 +391,7 @@ class ClientTest(unittest.TestCase): installer.supported_enhancements.return_value = ["redirect"] installer.save.side_effect = errors.PluginError - config = ConfigHelper(redirect=True, hsts=False, uir=False) + config = ConfigHelper(redirect=True, hsts=False, uir=False, must_staple=False) self.assertRaises(errors.PluginError, self.client.enhance_config, ["foo.bar"], config) @@ -408,7 +408,7 @@ class ClientTest(unittest.TestCase): installer.supported_enhancements.return_value = ["redirect"] installer.restart.side_effect = [errors.PluginError, None] - config = ConfigHelper(redirect=True, hsts=False, uir=False) + config = ConfigHelper(redirect=True, hsts=False, uir=False, must_staple=False) self.assertRaises(errors.PluginError, self.client.enhance_config, ["foo.bar"], config) @@ -428,7 +428,7 @@ class ClientTest(unittest.TestCase): installer.restart.side_effect = errors.PluginError installer.rollback_checkpoints.side_effect = errors.ReverterError - config = ConfigHelper(redirect=True, hsts=False, uir=False) + config = ConfigHelper(redirect=True, hsts=False, uir=False, must_staple=False) self.assertRaises(errors.PluginError, self.client.enhance_config, ["foo.bar"], config) From 911b4565bea1cb4dbcfc49a4c4ce4337c377e207 Mon Sep 17 00:00:00 2001 From: Amjad Mashaal Date: Wed, 25 May 2016 23:09:17 +0200 Subject: [PATCH 203/396] Moving check for conflicting args to cli.py --- certbot/cli.py | 10 ++++++++++ certbot/main.py | 10 ---------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/certbot/cli.py b/certbot/cli.py index 44712ada0..72ec94915 100644 --- a/certbot/cli.py +++ b/certbot/cli.py @@ -343,6 +343,16 @@ class HelpfulArgumentParser(object): if parsed_args.csr: self.handle_csr(parsed_args) + # Avoid conflicting args + conficting_args = ["quiet", "noninteractive_mode", "text_mode"] + if parsed_args.dialog_mode: + for arg in conficting_args: + if getattr(parsed_args, arg): + raise errors.Error( + ("Conflicting values for displayer." + " {0} conflicts with dialog_mode").format(arg) + ) + hooks.validate_hooks(parsed_args) return parsed_args diff --git a/certbot/main.py b/certbot/main.py index 99d06a69e..4ef2e6ac8 100644 --- a/certbot/main.py +++ b/certbot/main.py @@ -673,16 +673,6 @@ def main(cli_args=sys.argv[1:]): sys.excepthook = functools.partial(_handle_exception, config=config) - # Avoid conflicting args - conficting_args = ["quiet", "noninteractive_mode", "text_mode"] - if config.dialog_mode: - for arg in conficting_args: - if getattr(config, arg): - raise errors.Error( - ("Conflicting values for displayer." - " {0} conflicts with dialog_mode").format(arg) - ) - # Displayer if config.quiet: config.noninteractive_mode = True From 0b691d1b561c0bb5b068664935fb4fe4ed19dbdc Mon Sep 17 00:00:00 2001 From: Seth Schoen Date: Wed, 25 May 2016 14:11:12 -0700 Subject: [PATCH 204/396] Use better update_registration invocation, and reporter interface --- certbot/main.py | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/certbot/main.py b/certbot/main.py index fa5797483..3a3218a49 100644 --- a/certbot/main.py +++ b/certbot/main.py @@ -10,7 +10,6 @@ import traceback import zope.component -from acme import errors as acme_errors from acme import jose import certbot @@ -397,17 +396,12 @@ def register(config, unused_plugins): "required\n(hint: --email)") acc, acme = _determine_account(config) acme_client = client.Client(config, acc, None, None, acme=acme) - try: - updated_reg = client.messages.Registration.from_data(email=config.email) - acme_client.acme.update_registration(acme_client.account.regr, - updated_reg) - except acme_errors.UnexpectedUpdate: - # We expect the unexpected update! - pass - query_data = acme_client.acme.query_registration(acme_client.account.regr) - # We rely on an ACME exception to interrupt this process if it didn't work. - print("Registration change succeeded. New registration data:\n") - print(query_data) + data = acme_client.acme.update_registration(acc.regr.update( + body=acc.regr.body.update(contact=('mailto:' + config.email,)))) + # We rely on an exception to interrupt this process if it didn't work. + reporter_util = zope.component.getUtility(interfaces.IReporter) + msg = "Your e-mail address was updated to {0}.".format(config.email) + reporter_util.add_message(msg, reporter_util.HIGH_PRIORITY) return From 6ceaca4a9d26c87c732329b149a7f7301fe22ad0 Mon Sep 17 00:00:00 2001 From: Seth Schoen Date: Wed, 25 May 2016 14:11:43 -0700 Subject: [PATCH 205/396] Minor test updates for update_registration call change --- certbot/tests/cli_test.py | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/certbot/tests/cli_test.py b/certbot/tests/cli_test.py index 4dfc78792..007675759 100644 --- a/certbot/tests/cli_test.py +++ b/certbot/tests/cli_test.py @@ -947,25 +947,18 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods mocked_storage = mock.MagicMock() mocked_account.AccountFileStorage.return_value = mocked_storage mocked_storage.find_all.return_value = ["an account"] - mocked_det.return_value = ("a", "b") + mocked_det.return_value = (mock.MagicMock(), "foo") acme_client = mock.MagicMock() mocked_client.Client.return_value = acme_client - # Currently the update_registration() call always - # raises a harmless acme_errors.UnexpectedUpdate. - # If this is fixed, we should get rid of both this - # side effect and the corresponding try/catch in - # main.register(). - uu = acme_errors.UnexpectedUpdate - acme_client.acme.update_registration.side_effect = uu x = self._call_no_clientmock( ["register", "--update-registration", "--email", "user@example.org"]) # When registration change succeeds, the return value # of register() is None assert x[0] is None - # and we got far enough to query the registration from + # and we got supposedly did update the registration from # the server - assert acme_client.acme.query_registration.call_count == 1 + assert acme_client.acme.update_registration.call_count == 1 class DetermineAccountTest(unittest.TestCase): From 74f5c851782cf40b938f4e6389596d6c994d2f57 Mon Sep 17 00:00:00 2001 From: Amjad Mashaal Date: Wed, 25 May 2016 23:13:26 +0200 Subject: [PATCH 206/396] Moving tests for conflicting args to cli_test.py --- certbot/tests/cli_test.py | 4 ++++ certbot/tests/main_test.py | 39 +++----------------------------------- 2 files changed, 7 insertions(+), 36 deletions(-) diff --git a/certbot/tests/cli_test.py b/certbot/tests/cli_test.py index afaa53570..20d891324 100644 --- a/certbot/tests/cli_test.py +++ b/certbot/tests/cli_test.py @@ -908,6 +908,10 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods self._call(['-c', test_util.vector_path('cli.ini')]) self.assertTrue(mocked_run.called) + def test_conflicting_args(self): + args = ['renew', '--dialog', '--text'] + self.assertRaises(errors.Error, self._call, args) + class DetermineAccountTest(unittest.TestCase): """Tests for certbot.cli._determine_account.""" diff --git a/certbot/tests/main_test.py b/certbot/tests/main_test.py index e3f8b860d..66cba64a3 100644 --- a/certbot/tests/main_test.py +++ b/certbot/tests/main_test.py @@ -1,49 +1,15 @@ """Tests for certbot.main.""" -import os -import shutil -import tempfile import unittest + import mock -import six + from certbot import cli from certbot import configuration -from certbot import errors -from certbot import main from certbot.plugins import disco as plugins_disco -class MainTest(unittest.TestCase): - """Tests for certbot.main""" - - def setUp(self): - self.tmp_dir = tempfile.mkdtemp() - self.config_dir = os.path.join(self.tmp_dir, 'config') - self.work_dir = os.path.join(self.tmp_dir, 'work') - self.logs_dir = os.path.join(self.tmp_dir, 'logs') - self.standard_args = ['--config-dir', self.config_dir, - '--work-dir', self.work_dir, - '--logs-dir', self.logs_dir, '--text'] - - def tearDown(self): - shutil.rmtree(self.tmp_dir) - - def _call(self, args, stdout=None): - "Run the client with output streams mocked out" - args = self.standard_args + args - - toy_stdout = stdout if stdout else six.StringIO() - with mock.patch('certbot.main.sys.stdout', new=toy_stdout): - with mock.patch('certbot.main.sys.stderr') as stderr: - ret = main.main(args[:]) # NOTE: parser can alter its args! - return ret, toy_stdout, stderr - - def test_args_conflict(self): - args = ['renew', '--dialog', '--text'] - self.assertRaises(errors.Error, self._call, args) - - class ObtainCertTest(unittest.TestCase): """Tests for certbot.main.obtain_cert.""" @@ -60,6 +26,7 @@ class ObtainCertTest(unittest.TestCase): config = configuration.NamespaceConfig( cli.prepare_and_parse_args(plugins, args)) + from certbot import main with mock.patch('certbot.main._init_le_client') as mock_init: main.obtain_cert(config, plugins) From efcd0090da49c9649b835d1bd5e3350f17b19fcc Mon Sep 17 00:00:00 2001 From: sagi Date: Wed, 25 May 2016 21:20:13 +0000 Subject: [PATCH 207/396] Add a specific must-staple test --- certbot/tests/client_test.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/certbot/tests/client_test.py b/certbot/tests/client_test.py index e1aea1b64..ae66ab5d0 100644 --- a/certbot/tests/client_test.py +++ b/certbot/tests/client_test.py @@ -304,6 +304,25 @@ class ClientTest(unittest.TestCase): installer.rollback_checkpoints.assert_called_once_with() self.assertEqual(installer.restart.call_count, 1) + @mock.patch("certbot.client.enhancements") + def test_must_staple(self, mock_enhancements): + # Testing our wanted behaviour: Enable OCSP Stapling when the + # --must-staple flag is on. [Without having the --staple-ocsp flag on] + config = ConfigHelper(must_staple=True, staple=False) + self.assertRaises(errors.Error, + self.client.enhance_config, ["foo.bar"], config) + + mock_enhancements.ask.return_value = True + installer = mock.MagicMock() + self.client.installer = installer + installer.supported_enhancements.return_value = ["staple-ocsp"] + + self.client.enhance_config(["foo.bar"], config) + installer.enhance.assert_called_once_with("foo.bar", "staple-ocsp", None) + self.assertEqual(installer.save.call_count, 1) + installer.restart.assert_called_once_with() + + @mock.patch("certbot.client.enhancements") def test_enhance_config(self, mock_enhancements): config = ConfigHelper(redirect=True, hsts=False, uir=False, must_staple=False) From 12a356298c6217e5b9934262742ebed04c1ddfec Mon Sep 17 00:00:00 2001 From: Amjad Mashaal Date: Wed, 25 May 2016 20:34:38 +0200 Subject: [PATCH 208/396] Displaying DialogError details correctly --- certbot/main.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/certbot/main.py b/certbot/main.py index fa5d43b72..b5407370a 100644 --- a/certbot/main.py +++ b/certbot/main.py @@ -623,7 +623,11 @@ def _handle_exception(exc_type, exc_value, trace, config): # Here we're passing a client or ACME error out to the client at the shell # Tell the user a bit about what happened, without overwhelming # them with a full traceback - err = traceback.format_exception_only(exc_type, exc_value)[0] + from dialog import DialogError + if issubclass(exc_type, DialogError): + err = exc_value.complete_message() + else: + err = traceback.format_exception_only(exc_type, exc_value)[0] # Typical error from the ACME module: # acme.messages.Error: urn:acme:error:malformed :: The request message was # malformed :: Error creating new registration :: Validation of contact From b77d288adb0f78db6032a1d066a546d3c99ca9b0 Mon Sep 17 00:00:00 2001 From: sagi Date: Wed, 25 May 2016 21:49:53 +0000 Subject: [PATCH 209/396] Use cli.py to set .staple given .must_staple --- certbot/cli.py | 3 +++ certbot/tests/cli_test.py | 7 +++++++ 2 files changed, 10 insertions(+) diff --git a/certbot/cli.py b/certbot/cli.py index 3bd565496..05a189712 100644 --- a/certbot/cli.py +++ b/certbot/cli.py @@ -343,6 +343,9 @@ class HelpfulArgumentParser(object): if parsed_args.csr: self.handle_csr(parsed_args) + if parsed_args.must_staple: + parsed_args.staple = True + hooks.validate_hooks(parsed_args) return parsed_args diff --git a/certbot/tests/cli_test.py b/certbot/tests/cli_test.py index 4ae69e69d..00c9a0a26 100644 --- a/certbot/tests/cli_test.py +++ b/certbot/tests/cli_test.py @@ -422,6 +422,13 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods for arg in conflicting_args: self.assertTrue(arg in error.message) + def test_must_staple_flag(self): + parse = self._get_argument_parser() + short_args = ['--must-staple'] + namespace = parse(short_args) + self.assertTrue(namespace.must_staple) + self.assertTrue(namespace.staple) + def test_staging_flag(self): parse = self._get_argument_parser() short_args = ['--staging'] From 2268dbf48987e902bf01580d3748853afc6b2778 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 25 May 2016 14:55:44 -0700 Subject: [PATCH 210/396] delint --- certbot/main.py | 2 +- certbot/tests/cli_test.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/certbot/main.py b/certbot/main.py index 3a3218a49..b7dc88839 100644 --- a/certbot/main.py +++ b/certbot/main.py @@ -396,7 +396,7 @@ def register(config, unused_plugins): "required\n(hint: --email)") acc, acme = _determine_account(config) acme_client = client.Client(config, acc, None, None, acme=acme) - data = acme_client.acme.update_registration(acc.regr.update( + acme_client.acme.update_registration(acc.regr.update( body=acc.regr.body.update(contact=('mailto:' + config.email,)))) # We rely on an exception to interrupt this process if it didn't work. reporter_util = zope.component.getUtility(interfaces.IReporter) diff --git a/certbot/tests/cli_test.py b/certbot/tests/cli_test.py index 007675759..92caf8f04 100644 --- a/certbot/tests/cli_test.py +++ b/certbot/tests/cli_test.py @@ -14,7 +14,6 @@ import mock import six from six.moves import reload_module # pylint: disable=import-error -from acme import errors as acme_errors from acme import jose from certbot import account From 20be730a924e3a32c005556ce5a62ac1e279f1d0 Mon Sep 17 00:00:00 2001 From: sagi Date: Wed, 25 May 2016 21:56:15 +0000 Subject: [PATCH 211/396] Revert client, client_test back --- certbot/client.py | 5 ++--- certbot/tests/client_test.py | 41 ++++++++++-------------------------- 2 files changed, 13 insertions(+), 33 deletions(-) diff --git a/certbot/client.py b/certbot/client.py index 5d1338baf..ba31f8760 100644 --- a/certbot/client.py +++ b/certbot/client.py @@ -406,7 +406,6 @@ class Client(object): hsts = config.hsts if "ensure-http-header" in supported else False uir = config.uir if "ensure-http-header" in supported else False staple = config.staple if "staple-ocsp" in supported else False - must_staple = config.must_staple if redirect is None: redirect = enhancements.ask("redirect") @@ -420,11 +419,11 @@ class Client(object): if uir: self.apply_enhancement(domains, "ensure-http-header", "Upgrade-Insecure-Requests") - if staple or must_staple: + if staple: self.apply_enhancement(domains, "staple-ocsp") msg = ("We were unable to restart web server") - if redirect or hsts or uir or staple or must_staple: + if redirect or hsts or uir or staple: with error_handler.ErrorHandler(self._rollback_and_restart, msg): self.installer.restart() diff --git a/certbot/tests/client_test.py b/certbot/tests/client_test.py index ae66ab5d0..8490efd9f 100644 --- a/certbot/tests/client_test.py +++ b/certbot/tests/client_test.py @@ -304,28 +304,9 @@ class ClientTest(unittest.TestCase): installer.rollback_checkpoints.assert_called_once_with() self.assertEqual(installer.restart.call_count, 1) - @mock.patch("certbot.client.enhancements") - def test_must_staple(self, mock_enhancements): - # Testing our wanted behaviour: Enable OCSP Stapling when the - # --must-staple flag is on. [Without having the --staple-ocsp flag on] - config = ConfigHelper(must_staple=True, staple=False) - self.assertRaises(errors.Error, - self.client.enhance_config, ["foo.bar"], config) - - mock_enhancements.ask.return_value = True - installer = mock.MagicMock() - self.client.installer = installer - installer.supported_enhancements.return_value = ["staple-ocsp"] - - self.client.enhance_config(["foo.bar"], config) - installer.enhance.assert_called_once_with("foo.bar", "staple-ocsp", None) - self.assertEqual(installer.save.call_count, 1) - installer.restart.assert_called_once_with() - - @mock.patch("certbot.client.enhancements") def test_enhance_config(self, mock_enhancements): - config = ConfigHelper(redirect=True, hsts=False, uir=False, must_staple=False) + config = ConfigHelper(redirect=True, hsts=False, uir=False) self.assertRaises(errors.Error, self.client.enhance_config, ["foo.bar"], config) @@ -341,7 +322,7 @@ class ClientTest(unittest.TestCase): @mock.patch("certbot.client.enhancements") def test_enhance_config_no_ask(self, mock_enhancements): - config = ConfigHelper(redirect=True, hsts=False, uir=False, must_staple=False) + config = ConfigHelper(redirect=True, hsts=False, uir=False) self.assertRaises(errors.Error, self.client.enhance_config, ["foo.bar"], config) @@ -350,16 +331,16 @@ class ClientTest(unittest.TestCase): self.client.installer = installer installer.supported_enhancements.return_value = ["redirect", "ensure-http-header"] - config = ConfigHelper(redirect=True, hsts=False, uir=False, must_staple=False) + config = ConfigHelper(redirect=True, hsts=False, uir=False) self.client.enhance_config(["foo.bar"], config) installer.enhance.assert_called_with("foo.bar", "redirect", None) - config = ConfigHelper(redirect=False, hsts=True, uir=False, must_staple=False) + config = ConfigHelper(redirect=False, hsts=True, uir=False) self.client.enhance_config(["foo.bar"], config) installer.enhance.assert_called_with("foo.bar", "ensure-http-header", "Strict-Transport-Security") - config = ConfigHelper(redirect=False, hsts=False, uir=True, must_staple=False) + config = ConfigHelper(redirect=False, hsts=False, uir=True) self.client.enhance_config(["foo.bar"], config) installer.enhance.assert_called_with("foo.bar", "ensure-http-header", "Upgrade-Insecure-Requests") @@ -373,13 +354,13 @@ class ClientTest(unittest.TestCase): self.client.installer = installer installer.supported_enhancements.return_value = [] - config = ConfigHelper(redirect=None, hsts=True, uir=True, must_staple=False) + config = ConfigHelper(redirect=None, hsts=True, uir=True) self.client.enhance_config(["foo.bar"], config) installer.enhance.assert_not_called() mock_enhancements.ask.assert_not_called() def test_enhance_config_no_installer(self): - config = ConfigHelper(redirect=True, hsts=False, uir=False, must_staple=False) + config = ConfigHelper(redirect=True, hsts=False, uir=False) self.assertRaises(errors.Error, self.client.enhance_config, ["foo.bar"], config) @@ -393,7 +374,7 @@ class ClientTest(unittest.TestCase): installer.supported_enhancements.return_value = ["redirect"] installer.enhance.side_effect = errors.PluginError - config = ConfigHelper(redirect=True, hsts=False, uir=False, must_staple=False) + config = ConfigHelper(redirect=True, hsts=False, uir=False) self.assertRaises(errors.PluginError, self.client.enhance_config, ["foo.bar"], config) @@ -410,7 +391,7 @@ class ClientTest(unittest.TestCase): installer.supported_enhancements.return_value = ["redirect"] installer.save.side_effect = errors.PluginError - config = ConfigHelper(redirect=True, hsts=False, uir=False, must_staple=False) + config = ConfigHelper(redirect=True, hsts=False, uir=False) self.assertRaises(errors.PluginError, self.client.enhance_config, ["foo.bar"], config) @@ -427,7 +408,7 @@ class ClientTest(unittest.TestCase): installer.supported_enhancements.return_value = ["redirect"] installer.restart.side_effect = [errors.PluginError, None] - config = ConfigHelper(redirect=True, hsts=False, uir=False, must_staple=False) + config = ConfigHelper(redirect=True, hsts=False, uir=False) self.assertRaises(errors.PluginError, self.client.enhance_config, ["foo.bar"], config) @@ -447,7 +428,7 @@ class ClientTest(unittest.TestCase): installer.restart.side_effect = errors.PluginError installer.rollback_checkpoints.side_effect = errors.ReverterError - config = ConfigHelper(redirect=True, hsts=False, uir=False, must_staple=False) + config = ConfigHelper(redirect=True, hsts=False, uir=False) self.assertRaises(errors.PluginError, self.client.enhance_config, ["foo.bar"], config) From 29822aad9d2a76780178d2e1f493ef4b5966d40f Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 25 May 2016 14:57:01 -0700 Subject: [PATCH 212/396] denit --- certbot/main.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/certbot/main.py b/certbot/main.py index b7dc88839..56d09c078 100644 --- a/certbot/main.py +++ b/certbot/main.py @@ -401,8 +401,7 @@ def register(config, unused_plugins): # We rely on an exception to interrupt this process if it didn't work. reporter_util = zope.component.getUtility(interfaces.IReporter) msg = "Your e-mail address was updated to {0}.".format(config.email) - reporter_util.add_message(msg, reporter_util.HIGH_PRIORITY) - return + reporter_util.add_message(msg, reporter_util.MEDIUM_PRIORITY) def install(config, plugins): From d57353a6fea562b00cc3e37e71b76e12ff380fef Mon Sep 17 00:00:00 2001 From: sagi Date: Wed, 25 May 2016 22:01:43 +0000 Subject: [PATCH 213/396] Add missing space. --- certbot/interfaces.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certbot/interfaces.py b/certbot/interfaces.py index 19d9f0c07..37835462e 100644 --- a/certbot/interfaces.py +++ b/certbot/interfaces.py @@ -201,7 +201,7 @@ class IConfig(zope.interface.Interface): "Email used for registration and recovery contact.") rsa_key_size = zope.interface.Attribute("Size of the RSA key.") must_staple = zope.interface.Attribute( - "Adds the OCSP Must Staple extension to the certificate." + "Adds the OCSP Must Staple extension to the certificate. " "Autoconfigures OCSP Stapling for supported setups " "(Apache version >= 2.3.3 ).") From 94588b1a9175af5f5943bfb8b9485c3bab376a5c Mon Sep 17 00:00:00 2001 From: Jacob Hoffman-Andrews Date: Wed, 25 May 2016 14:43:10 -0700 Subject: [PATCH 214/396] Check out a specific version of Boulder. A recent Boulder change broke integration tests, this fixes it. --- tests/boulder-fetch.sh | 2 +- tests/letstest/scripts/boulder_install.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/boulder-fetch.sh b/tests/boulder-fetch.sh index a09d0adf9..01f236575 100755 --- a/tests/boulder-fetch.sh +++ b/tests/boulder-fetch.sh @@ -3,7 +3,7 @@ set -xe # Check out special branch until latest docker changes land in Boulder master. -git clone https://github.com/letsencrypt/boulder $BOULDERPATH +git clone -b 71e4af43f792f33e6ab1aa87ddc23a6a368889f2 https://github.com/letsencrypt/boulder $BOULDERPATH cd $BOULDERPATH sed -i 's/FAKE_DNS: .*/FAKE_DNS: 172.17.42.1/' docker-compose.yml docker-compose up -d diff --git a/tests/letstest/scripts/boulder_install.sh b/tests/letstest/scripts/boulder_install.sh index 426642880..0d6153a2d 100755 --- a/tests/letstest/scripts/boulder_install.sh +++ b/tests/letstest/scripts/boulder_install.sh @@ -3,7 +3,7 @@ # >>>> only tested on Ubuntu 14.04LTS <<<< # Check out special branch until latest docker changes land in Boulder master. -git clone https://github.com/letsencrypt/boulder $BOULDERPATH +git clone -b 71e4af43f792f33e6ab1aa87ddc23a6a368889f2 https://github.com/letsencrypt/boulder $BOULDERPATH cd $BOULDERPATH sed -i 's/FAKE_DNS: .*/FAKE_DNS: 172.17.42.1/' docker-compose.yml docker-compose up -d From 0fb3704dcedf66f67b517b22833564f00cf74c48 Mon Sep 17 00:00:00 2001 From: Jacob Hoffman-Andrews Date: Wed, 25 May 2016 15:43:54 -0700 Subject: [PATCH 215/396] Use a real branch name. --- tests/boulder-fetch.sh | 2 +- tests/letstest/scripts/boulder_install.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/boulder-fetch.sh b/tests/boulder-fetch.sh index 01f236575..469c5cd80 100755 --- a/tests/boulder-fetch.sh +++ b/tests/boulder-fetch.sh @@ -3,7 +3,7 @@ set -xe # Check out special branch until latest docker changes land in Boulder master. -git clone -b 71e4af43f792f33e6ab1aa87ddc23a6a368889f2 https://github.com/letsencrypt/boulder $BOULDERPATH +git clone -b docker-integration https://github.com/letsencrypt/boulder $BOULDERPATH cd $BOULDERPATH sed -i 's/FAKE_DNS: .*/FAKE_DNS: 172.17.42.1/' docker-compose.yml docker-compose up -d diff --git a/tests/letstest/scripts/boulder_install.sh b/tests/letstest/scripts/boulder_install.sh index 0d6153a2d..7e298783f 100755 --- a/tests/letstest/scripts/boulder_install.sh +++ b/tests/letstest/scripts/boulder_install.sh @@ -3,7 +3,7 @@ # >>>> only tested on Ubuntu 14.04LTS <<<< # Check out special branch until latest docker changes land in Boulder master. -git clone -b 71e4af43f792f33e6ab1aa87ddc23a6a368889f2 https://github.com/letsencrypt/boulder $BOULDERPATH +git clone -b docker-integration https://github.com/letsencrypt/boulder $BOULDERPATH cd $BOULDERPATH sed -i 's/FAKE_DNS: .*/FAKE_DNS: 172.17.42.1/' docker-compose.yml docker-compose up -d From 14e2ea92ee21fa1bea1b7b5ae07b47a7a29dade0 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 25 May 2016 16:32:03 -0700 Subject: [PATCH 216/396] Add a way to only save registration resources --- certbot/account.py | 23 ++++++++++++++++++----- certbot/tests/account_test.py | 10 ++++++++++ 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/certbot/account.py b/certbot/account.py index cc50a6ea6..5e0d4f2fd 100644 --- a/certbot/account.py +++ b/certbot/account.py @@ -186,16 +186,29 @@ class AccountFileStorage(interfaces.AccountStorage): return acc def save(self, account): + self._save(account, regr_only=False) + + def save_regr(self, account): + """Save the registration resource. + + :param Account account: account whose regr should be saved + + """ + self._save(account, regr_only=True) + + def _save(self, account, regr_only): account_dir_path = self._account_dir_path(account.id) le_util.make_or_verify_dir(account_dir_path, 0o700, os.geteuid(), self.config.strict_permissions) try: with open(self._regr_path(account_dir_path), "w") as regr_file: regr_file.write(account.regr.json_dumps()) - with le_util.safe_open(self._key_path(account_dir_path), - "w", chmod=0o400) as key_file: - key_file.write(account.key.json_dumps()) - with open(self._metadata_path(account_dir_path), "w") as metadata_file: - metadata_file.write(account.meta.json_dumps()) + if not regr_only: + with le_util.safe_open(self._key_path(account_dir_path), + "w", chmod=0o400) as key_file: + key_file.write(account.key.json_dumps()) + with open(self._metadata_path( + account_dir_path), "w") as metadata_file: + metadata_file.write(account.meta.json_dumps()) except IOError as error: raise errors.AccountStorageError(error) diff --git a/certbot/tests/account_test.py b/certbot/tests/account_test.py index a96e57507..4cd2bfebf 100644 --- a/certbot/tests/account_test.py +++ b/certbot/tests/account_test.py @@ -137,6 +137,16 @@ class AccountFileStorageTest(unittest.TestCase): # restore self.assertEqual(self.acc, self.storage.load(self.acc.id)) + def test_save_regr(self): + self.storage.save_regr(self.acc) + account_path = os.path.join(self.config.accounts_dir, self.acc.id) + self.assertTrue(os.path.exists(account_path)) + self.assertTrue(os.path.exists(os.path.join( + account_path, "regr.json"))) + for file_name in "meta.json", "private_key.json": + self.assertFalse(os.path.exists( + os.path.join(account_path, file_name))) + def test_find_all(self): self.storage.save(self.acc) self.assertEqual([self.acc], self.storage.find_all()) From 5531c156e8511c0528101452c7a5f71d3952175f Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 25 May 2016 16:41:19 -0700 Subject: [PATCH 217/396] Save the updated registration --- certbot/main.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/certbot/main.py b/certbot/main.py index 56d09c078..3491f44a6 100644 --- a/certbot/main.py +++ b/certbot/main.py @@ -396,9 +396,10 @@ def register(config, unused_plugins): "required\n(hint: --email)") acc, acme = _determine_account(config) acme_client = client.Client(config, acc, None, None, acme=acme) - acme_client.acme.update_registration(acc.regr.update( - body=acc.regr.body.update(contact=('mailto:' + config.email,)))) # We rely on an exception to interrupt this process if it didn't work. + acc.regr = acme_client.acme.update_registration(acc.regr.update( + body=acc.regr.body.update(contact=('mailto:' + config.email,)))) + account_storage.save_regr(account) reporter_util = zope.component.getUtility(interfaces.IReporter) msg = "Your e-mail address was updated to {0}.".format(config.email) reporter_util.add_message(msg, reporter_util.MEDIUM_PRIORITY) From a87df33de69c9105888e8ec857e36af5c000b58d Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 25 May 2016 16:41:39 -0700 Subject: [PATCH 218/396] Update register tests --- certbot/tests/cli_test.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/certbot/tests/cli_test.py b/certbot/tests/cli_test.py index 92caf8f04..fbb822490 100644 --- a/certbot/tests/cli_test.py +++ b/certbot/tests/cli_test.py @@ -914,7 +914,7 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods mocked_account.AccountFileStorage.return_value = mocked_storage mocked_storage.find_all.return_value = ["an account"] x = self._call_no_clientmock(["register", "--email", "user@example.org"]) - assert "There is an existing account" in x[0] + self.assertTrue("There is an existing account" in x[0]) def test_update_registration_no_existing_accounts(self): # with mock.patch('certbot.main.client') as mocked_client: @@ -924,8 +924,8 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods mocked_storage.find_all.return_value = [] x = self._call_no_clientmock( ["register", "--update-registration", "--email", - "user@example.org"]) - assert "Could not find an existing account" in x[0] + "user@example.org"]) + self.assertTrue("Could not find an existing account" in x[0]) def test_update_registration_no_email(self): # This test will become obsolete when register --update-registration @@ -936,7 +936,7 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods mocked_account.AccountFileStorage.return_value = mocked_storage mocked_storage.find_all.return_value = ["an account"] x = self._call_no_clientmock(["register", "--update-registration"]) - assert "can only change the e-mail" in x[0] + self.assertTrue("can only change the e-mail" in x[0]) def test_update_registration_with_email(self): with mock.patch('certbot.main.client') as mocked_client: @@ -951,13 +951,16 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods mocked_client.Client.return_value = acme_client x = self._call_no_clientmock( ["register", "--update-registration", "--email", - "user@example.org"]) + "user@example.org"]) # When registration change succeeds, the return value # of register() is None - assert x[0] is None + self.assertTrue(x[0] is None) # and we got supposedly did update the registration from # the server - assert acme_client.acme.update_registration.call_count == 1 + self.assertTrue( + acme_client.acme.update_registration.called) + # and we saved the updated registration on disk + self.assertTrue(mocked_storage.save_regr.called) class DetermineAccountTest(unittest.TestCase): From 1819b22ebc11b917cc59ea503001596657f00234 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 25 May 2016 16:47:05 -0700 Subject: [PATCH 219/396] Fix typo --- certbot/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certbot/main.py b/certbot/main.py index 3491f44a6..20b9a7ce5 100644 --- a/certbot/main.py +++ b/certbot/main.py @@ -399,7 +399,7 @@ def register(config, unused_plugins): # We rely on an exception to interrupt this process if it didn't work. acc.regr = acme_client.acme.update_registration(acc.regr.update( body=acc.regr.body.update(contact=('mailto:' + config.email,)))) - account_storage.save_regr(account) + account_storage.save_regr(acc) reporter_util = zope.component.getUtility(interfaces.IReporter) msg = "Your e-mail address was updated to {0}.".format(config.email) reporter_util.add_message(msg, reporter_util.MEDIUM_PRIORITY) From d9d23772422153a58328556b305e626fe61ac898 Mon Sep 17 00:00:00 2001 From: Blake Griffith Date: Wed, 25 May 2016 18:28:40 -0500 Subject: [PATCH 220/396] Rename certbot.le_util to certbot.util Also rename certbot/tests/le_util_test.py to certbot/tests/util_test.py --- certbot-apache/certbot_apache/configurator.py | 19 +++--- certbot-apache/certbot_apache/constants.py | 4 +- certbot-nginx/certbot_nginx/configurator.py | 14 ++-- certbot/account.py | 8 +-- certbot/cli.py | 6 +- certbot/client.py | 22 +++--- certbot/colored_logging.py | 4 +- certbot/configuration.py | 4 +- certbot/crypto_util.py | 27 ++++---- certbot/display/ops.py | 8 +-- certbot/main.py | 8 +-- certbot/plugins/common.py | 4 +- certbot/plugins/common_test.py | 2 +- certbot/renewal.py | 4 +- certbot/reporter.py | 8 +-- certbot/reverter.py | 10 +-- certbot/storage.py | 8 +-- certbot/tests/auth_handler_test.py | 4 +- certbot/tests/cli_test.py | 8 +-- certbot/tests/client_test.py | 6 +- certbot/tests/colored_logging_test.py | 6 +- certbot/tests/crypto_util_test.py | 16 ++--- certbot/tests/display/ops_test.py | 6 +- certbot/tests/reverter_test.py | 2 +- certbot/tests/storage_test.py | 2 +- .../tests/{le_util_test.py => util_test.py} | 68 +++++++++---------- certbot/{le_util.py => util.py} | 0 27 files changed, 138 insertions(+), 140 deletions(-) rename certbot/tests/{le_util_test.py => util_test.py} (85%) rename certbot/{le_util.py => util.py} (100%) diff --git a/certbot-apache/certbot_apache/configurator.py b/certbot-apache/certbot_apache/configurator.py index 1e02ae7b3..e4c06ba7e 100644 --- a/certbot-apache/certbot_apache/configurator.py +++ b/certbot-apache/certbot_apache/configurator.py @@ -15,7 +15,7 @@ from acme import challenges from certbot import errors from certbot import interfaces -from certbot import le_util +from certbot import util from certbot.plugins import common @@ -106,8 +106,8 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): add("handle-sites", default=constants.os_constant("handle_sites"), help="Let installer handle enabling sites for you." + "(Only Ubuntu/Debian currently)") - le_util.add_deprecated_argument(add, argument_name="ctl", nargs=1) - le_util.add_deprecated_argument( + util.add_deprecated_argument(add, argument_name="ctl", nargs=1) + util.add_deprecated_argument( add, argument_name="init-script", nargs=1) def __init__(self, *args, **kwargs): @@ -151,7 +151,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): """ # Verify Apache is installed - if not le_util.exe_exists(constants.os_constant("restart_cmd")[0]): + if not util.exe_exists(constants.os_constant("restart_cmd")[0]): raise errors.NoInstallationError # Make sure configuration is valid @@ -1521,14 +1521,14 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): # Generate reversal command. # Try to be safe here... check that we can probably reverse before # applying enmod command - if not le_util.exe_exists(self.conf("dismod")): + if not util.exe_exists(self.conf("dismod")): raise errors.MisconfigurationError( "Unable to find a2dismod, please make sure a2enmod and " "a2dismod are configured correctly for certbot.") self.reverter.register_undo_command( temp, [self.conf("dismod"), mod_name]) - le_util.run_script([self.conf("enmod"), mod_name]) + util.run_script([self.conf("enmod"), mod_name]) def restart(self): """Runs a config test and reloads the Apache server. @@ -1547,7 +1547,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): """ try: - le_util.run_script(constants.os_constant("restart_cmd")) + util.run_script(constants.os_constant("restart_cmd")) except errors.SubprocessError as err: raise errors.MisconfigurationError(str(err)) @@ -1558,7 +1558,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): """ try: - le_util.run_script(constants.os_constant("conftest_cmd")) + util.run_script(constants.os_constant("conftest_cmd")) except errors.SubprocessError as err: raise errors.MisconfigurationError(str(err)) @@ -1574,8 +1574,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): """ try: - stdout, _ = le_util.run_script( - constants.os_constant("version_cmd")) + stdout, _ = util.run_script(constants.os_constant("version_cmd")) except errors.SubprocessError: raise errors.PluginError( "Unable to run %s -v" % diff --git a/certbot-apache/certbot_apache/constants.py b/certbot-apache/certbot_apache/constants.py index f3226572c..faf74394d 100644 --- a/certbot-apache/certbot_apache/constants.py +++ b/certbot-apache/certbot_apache/constants.py @@ -1,6 +1,6 @@ """Apache plugin constants.""" import pkg_resources -from certbot import le_util +from certbot import util CLI_DEFAULTS_DEBIAN = dict( @@ -116,7 +116,7 @@ def os_constant(key): :param key: name of cli constant :return: value of constant for active os """ - os_info = le_util.get_os_info() + os_info = util.get_os_info() try: constants = CLI_DEFAULTS[os_info[0].lower()] except KeyError: diff --git a/certbot-nginx/certbot_nginx/configurator.py b/certbot-nginx/certbot_nginx/configurator.py index e402d5c79..30928e56c 100644 --- a/certbot-nginx/certbot_nginx/configurator.py +++ b/certbot-nginx/certbot_nginx/configurator.py @@ -17,7 +17,7 @@ from certbot import constants as core_constants from certbot import crypto_util from certbot import errors from certbot import interfaces -from certbot import le_util +from certbot import util from certbot import reverter from certbot.plugins import common @@ -111,7 +111,7 @@ class NginxConfigurator(common.Plugin): :raises .errors.MisconfigurationError: If Nginx is misconfigured """ # Verify Nginx is installed - if not le_util.exe_exists(self.conf('ctl')): + if not util.exe_exists(self.conf('ctl')): raise errors.NoInstallationError # Make sure configuration is valid @@ -318,7 +318,7 @@ class NginxConfigurator(common.Plugin): cert = acme_crypto_util.gen_ss_cert(key, domains=[socket.gethostname()]) cert_pem = OpenSSL.crypto.dump_certificate( OpenSSL.crypto.FILETYPE_PEM, cert) - cert_file, cert_path = le_util.unique_file(os.path.join(tmp_dir, "cert.pem")) + cert_file, cert_path = util.unique_file(os.path.join(tmp_dir, "cert.pem")) with cert_file: cert_file.write(cert_pem) return cert_path, le_key.file @@ -426,7 +426,7 @@ class NginxConfigurator(common.Plugin): """ try: - le_util.run_script([self.conf('ctl'), "-c", self.nginx_conf, "-t"]) + util.run_script([self.conf('ctl'), "-c", self.nginx_conf, "-t"]) except errors.SubprocessError as err: raise errors.MisconfigurationError(str(err)) @@ -439,11 +439,11 @@ class NginxConfigurator(common.Plugin): """ uid = os.geteuid() - le_util.make_or_verify_dir( + util.make_or_verify_dir( self.config.work_dir, core_constants.CONFIG_DIRS_MODE, uid) - le_util.make_or_verify_dir( + util.make_or_verify_dir( self.config.backup_dir, core_constants.CONFIG_DIRS_MODE, uid) - le_util.make_or_verify_dir( + util.make_or_verify_dir( self.config.config_dir, core_constants.CONFIG_DIRS_MODE, uid) def get_version(self): diff --git a/certbot/account.py b/certbot/account.py index cc50a6ea6..2ef3629e2 100644 --- a/certbot/account.py +++ b/certbot/account.py @@ -16,7 +16,7 @@ from acme import messages from certbot import errors from certbot import interfaces -from certbot import le_util +from certbot import util logger = logging.getLogger(__name__) @@ -130,7 +130,7 @@ class AccountFileStorage(interfaces.AccountStorage): """ def __init__(self, config): self.config = config - le_util.make_or_verify_dir(config.accounts_dir, 0o700, os.geteuid(), + util.make_or_verify_dir(config.accounts_dir, 0o700, os.geteuid(), self.config.strict_permissions) def _account_dir_path(self, account_id): @@ -187,12 +187,12 @@ class AccountFileStorage(interfaces.AccountStorage): def save(self, account): account_dir_path = self._account_dir_path(account.id) - le_util.make_or_verify_dir(account_dir_path, 0o700, os.geteuid(), + util.make_or_verify_dir(account_dir_path, 0o700, os.geteuid(), self.config.strict_permissions) try: with open(self._regr_path(account_dir_path), "w") as regr_file: regr_file.write(account.regr.json_dumps()) - with le_util.safe_open(self._key_path(account_dir_path), + with util.safe_open(self._key_path(account_dir_path), "w", chmod=0o400) as key_file: key_file.write(account.key.json_dumps()) with open(self._metadata_path(account_dir_path), "w") as metadata_file: diff --git a/certbot/cli.py b/certbot/cli.py index 05a189712..e8f4a16f0 100644 --- a/certbot/cli.py +++ b/certbot/cli.py @@ -17,7 +17,7 @@ from certbot import crypto_util from certbot import errors from certbot import hooks from certbot import interfaces -from certbot import le_util +from certbot import util from certbot.plugins import disco as plugins_disco import certbot.plugins.selection as plugin_selection @@ -505,7 +505,7 @@ class HelpfulArgumentParser(object): :param int nargs: Number of arguments the option takes. """ - le_util.add_deprecated_argument( + util.add_deprecated_argument( self.parser.add_argument, argument_name, num_args) def add_group(self, topic, **kwargs): @@ -938,7 +938,7 @@ def add_domains(args_or_config, domains): """ validated_domains = [] for domain in domains.split(","): - domain = le_util.enforce_domain_sanity(domain.strip()) + domain = util.enforce_domain_sanity(domain.strip()) validated_domains.append(domain) if domain not in args_or_config.domains: args_or_config.domains.append(domain) diff --git a/certbot/client.py b/certbot/client.py index ba31f8760..3d17e2295 100644 --- a/certbot/client.py +++ b/certbot/client.py @@ -21,7 +21,7 @@ from certbot import crypto_util from certbot import errors from certbot import error_handler from certbot import interfaces -from certbot import le_util +from certbot import util from certbot import reverter from certbot import storage from certbot import cli @@ -53,7 +53,7 @@ def _determine_user_agent(config): if config.user_agent is None: ua = "CertbotACMEClient/{0} ({1}) Authenticator/{2} Installer/{3}" - ua = ua.format(certbot.__version__, " ".join(le_util.get_os_info()), + ua = ua.format(certbot.__version__, " ".join(util.get_os_info()), config.authenticator, config.installer) else: ua = config.user_agent @@ -198,7 +198,7 @@ class Client(object): consistent with identifiers present in the `csr`. :param list domains: Domain names. - :param .le_util.CSR csr: DER-encoded Certificate Signing + :param .util.CSR csr: DER-encoded Certificate Signing Request. The key used to generate this CSR can be different than `authkey`. :param list authzr: List of @@ -237,8 +237,8 @@ class Client(object): :returns: `.CertificateResource`, certificate chain (as returned by `.fetch_chain`), and newly generated private key - (`.le_util.Key`) and DER-encoded Certificate Signing Request - (`.le_util.CSR`). + (`.util.Key`) and DER-encoded Certificate Signing Request + (`.util.CSR`). :rtype: tuple """ @@ -312,7 +312,7 @@ class Client(object): """ for path in cert_path, chain_path, fullchain_path: - le_util.make_or_verify_dir( + util.make_or_verify_dir( os.path.dirname(path), 0o755, os.geteuid(), self.config.strict_permissions) @@ -504,9 +504,9 @@ def validate_key_csr(privkey, csr=None): If csr is left as None, only the key will be validated. :param privkey: Key associated with CSR - :type privkey: :class:`certbot.le_util.Key` + :type privkey: :class:`certbot.util.Key` - :param .le_util.CSR csr: CSR + :param .util.CSR csr: CSR :raises .errors.Error: when validation fails @@ -523,7 +523,7 @@ def validate_key_csr(privkey, csr=None): if csr.form == "der": csr_obj = OpenSSL.crypto.load_certificate_request( OpenSSL.crypto.FILETYPE_ASN1, csr.data) - csr = le_util.CSR(csr.file, OpenSSL.crypto.dump_certificate( + csr = util.CSR(csr.file, OpenSSL.crypto.dump_certificate( OpenSSL.crypto.FILETYPE_PEM, csr_obj), "pem") # If CSR is provided, it must be readable and valid. @@ -586,10 +586,10 @@ def _open_pem_file(cli_arg_path, pem_path): """ if cli.set_by_cli(cli_arg_path): - return le_util.safe_open(pem_path, chmod=0o644),\ + return util.safe_open(pem_path, chmod=0o644),\ os.path.abspath(pem_path) else: - uniq = le_util.unique_file(pem_path, 0o644) + uniq = util.unique_file(pem_path, 0o644) return uniq[0], os.path.abspath(uniq[1]) def _save_chain(chain_pem, chain_file): diff --git a/certbot/colored_logging.py b/certbot/colored_logging.py index d42fb5966..93bf3a55a 100644 --- a/certbot/colored_logging.py +++ b/certbot/colored_logging.py @@ -2,7 +2,7 @@ import logging import sys -from certbot import le_util +from certbot import util class StreamHandler(logging.StreamHandler): @@ -40,6 +40,6 @@ class StreamHandler(logging.StreamHandler): if sys.version_info < (2, 7) else super(StreamHandler, self).format(record)) if self.colored and record.levelno >= self.red_level: - return ''.join((le_util.ANSI_SGR_RED, out, le_util.ANSI_SGR_RESET)) + return ''.join((util.ANSI_SGR_RED, out, util.ANSI_SGR_RESET)) else: return out diff --git a/certbot/configuration.py b/certbot/configuration.py index 172b35bfe..712135b8d 100644 --- a/certbot/configuration.py +++ b/certbot/configuration.py @@ -8,7 +8,7 @@ import zope.interface from certbot import constants from certbot import errors from certbot import interfaces -from certbot import le_util +from certbot import util @zope.interface.implementer(interfaces.IConfig) @@ -132,4 +132,4 @@ def check_config_sanity(config): if config.namespace.domains is not None: for domain in config.namespace.domains: # This may be redundant, but let's be paranoid - le_util.enforce_domain_sanity(domain) + util.enforce_domain_sanity(domain) diff --git a/certbot/crypto_util.py b/certbot/crypto_util.py index 68e07e059..6b1b8426c 100644 --- a/certbot/crypto_util.py +++ b/certbot/crypto_util.py @@ -17,7 +17,7 @@ from acme import jose from certbot import errors from certbot import interfaces -from certbot import le_util +from certbot import util logger = logging.getLogger(__name__) @@ -37,7 +37,7 @@ def init_save_key(key_size, key_dir, keyname="key-certbot.pem"): :param str keyname: Filename of key :returns: Key - :rtype: :class:`certbot.le_util.Key` + :rtype: :class:`certbot.util.Key` :raises ValueError: If unable to generate the key given key_size. @@ -50,30 +50,29 @@ def init_save_key(key_size, key_dir, keyname="key-certbot.pem"): config = zope.component.getUtility(interfaces.IConfig) # Save file - le_util.make_or_verify_dir(key_dir, 0o700, os.geteuid(), - config.strict_permissions) - key_f, key_path = le_util.unique_file( - os.path.join(key_dir, keyname), 0o600) + util.make_or_verify_dir(key_dir, 0o700, os.geteuid(), + config.strict_permissions) + key_f, key_path = util.unique_file(os.path.join(key_dir, keyname), 0o600) with key_f: key_f.write(key_pem) logger.info("Generating key (%d bits): %s", key_size, key_path) - return le_util.Key(key_path, key_pem) + return util.Key(key_path, key_pem) def init_save_csr(privkey, names, path, csrname="csr-certbot.pem"): """Initialize a CSR with the given private key. :param privkey: Key to include in the CSR - :type privkey: :class:`certbot.le_util.Key` + :type privkey: :class:`certbot.util.Key` :param set names: `str` names to include in the CSR :param str path: Certificate save directory. :returns: CSR - :rtype: :class:`certbot.le_util.CSR` + :rtype: :class:`certbot.util.CSR` """ config = zope.component.getUtility(interfaces.IConfig) @@ -82,16 +81,16 @@ def init_save_csr(privkey, names, path, csrname="csr-certbot.pem"): must_staple=config.must_staple) # Save CSR - le_util.make_or_verify_dir(path, 0o755, os.geteuid(), + util.make_or_verify_dir(path, 0o755, os.geteuid(), config.strict_permissions) - csr_f, csr_filename = le_util.unique_file( + csr_f, csr_filename = util.unique_file( os.path.join(path, csrname), 0o644) csr_f.write(csr_pem) csr_f.close() logger.info("Creating CSR: %s", csr_filename) - return le_util.CSR(csr_filename, csr_der, "der") + return util.CSR(csr_filename, csr_der, "der") # Lower level functions @@ -187,7 +186,7 @@ def import_csr_file(csrfile, data): :param str data: contents of the CSR file :returns: (`OpenSSL.crypto.FILETYPE_PEM` or `OpenSSL.crypto.FILETYPE_ASN1`, - le_util.CSR object representing the CSR, + util.CSR object representing the CSR, list of domains requested in the CSR) :rtype: tuple @@ -200,7 +199,7 @@ def import_csr_file(csrfile, data): logger.debug("CSR parse error (form=%s, typ=%s):", form, typ) logger.debug(traceback.format_exc()) continue - return typ, le_util.CSR(file=csrfile, data=data, form=form), domains + return typ, util.CSR(file=csrfile, data=data, form=form), domains raise errors.Error("Failed to parse CSR file: {0}".format(csrfile)) diff --git a/certbot/display/ops.py b/certbot/display/ops.py index 6752bf0c1..a8f2283fc 100644 --- a/certbot/display/ops.py +++ b/certbot/display/ops.py @@ -6,7 +6,7 @@ import zope.component from certbot import errors from certbot import interfaces -from certbot import le_util +from certbot import util from certbot.display import util as display_util logger = logging.getLogger(__name__) @@ -42,7 +42,7 @@ def get_email(more=False, invalid=False): raise errors.MissingCommandlineFlag(msg) if code == display_util.OK: - if le_util.safe_email(email): + if util.safe_email(email): return email else: # TODO catch the server's ACME invalid email address error, and @@ -119,7 +119,7 @@ def get_valid_domains(domains): valid_domains = [] for domain in domains: try: - valid_domains.append(le_util.enforce_domain_sanity(domain)) + valid_domains.append(util.enforce_domain_sanity(domain)) except errors.ConfigurationError: continue return valid_domains @@ -163,7 +163,7 @@ def _choose_names_manually(): for i, domain in enumerate(domain_list): try: - domain_list[i] = le_util.enforce_domain_sanity(domain) + domain_list[i] = util.enforce_domain_sanity(domain) except errors.ConfigurationError as e: invalid_domains[domain] = e.message diff --git a/certbot/main.py b/certbot/main.py index 4ef2e6ac8..933a102d8 100644 --- a/certbot/main.py +++ b/certbot/main.py @@ -24,7 +24,7 @@ from certbot import constants from certbot import errors from certbot import hooks from certbot import interfaces -from certbot import le_util +from certbot import util from certbot import log from certbot import reporter from certbot import renewal @@ -229,7 +229,7 @@ def _find_duplicative_certs(config, domains): cli_config = configuration.RenewerConfiguration(config) configs_dir = cli_config.renewal_configs_dir # Verify the directory is there - le_util.make_or_verify_dir(configs_dir, mode=0o755, uid=os.geteuid()) + util.make_or_verify_dir(configs_dir, mode=0o755, uid=os.geteuid()) for renewal_file in renewal.renewal_conf_files(cli_config): try: @@ -656,12 +656,12 @@ def main(cli_args=sys.argv[1:]): # Setup logging ASAP, otherwise "No handlers could be found for # logger ..." TODO: this should be done before plugins discovery for directory in config.config_dir, config.work_dir: - le_util.make_or_verify_dir( + util.make_or_verify_dir( directory, constants.CONFIG_DIRS_MODE, os.geteuid(), "--strict-permissions" in cli_args) # TODO: logs might contain sensitive data such as contents of the # private key! #525 - le_util.make_or_verify_dir( + util.make_or_verify_dir( config.logs_dir, 0o700, os.geteuid(), "--strict-permissions" in cli_args) setup_logging(config, _cli_log_handler, logfile='letsencrypt.log') cli.possible_deprecation_warning(config) diff --git a/certbot/plugins/common.py b/certbot/plugins/common.py index 757bf19d8..007105c7b 100644 --- a/certbot/plugins/common.py +++ b/certbot/plugins/common.py @@ -12,7 +12,7 @@ from acme.jose import util as jose_util from certbot import constants from certbot import interfaces -from certbot import le_util +from certbot import util def option_namespace(name): @@ -255,7 +255,7 @@ class TLSSNI01(object): # Write out challenge cert and key with open(cert_path, "wb") as cert_chall_fd: cert_chall_fd.write(cert_pem) - with le_util.safe_open(key_path, 'wb', chmod=0o400) as key_file: + with util.safe_open(key_path, 'wb', chmod=0o400) as key_file: key_file.write(key_pem) return response diff --git a/certbot/plugins/common_test.py b/certbot/plugins/common_test.py index 0dd1cd522..f3ea714c4 100644 --- a/certbot/plugins/common_test.py +++ b/certbot/plugins/common_test.py @@ -193,7 +193,7 @@ class TLSSNI01Test(unittest.TestCase): with mock.patch("certbot.plugins.common.open", mock_open, create=True): - with mock.patch("certbot.plugins.common.le_util.safe_open", + with mock.patch("certbot.plugins.common.util.safe_open", mock_safe_open): # pylint: disable=protected-access self.assertEqual(response, self.sni._setup_challenge_cert( diff --git a/certbot/renewal.py b/certbot/renewal.py index b5b982972..d04e2d27c 100644 --- a/certbot/renewal.py +++ b/certbot/renewal.py @@ -18,7 +18,7 @@ from certbot import constants from certbot import crypto_util from certbot import errors from certbot import interfaces -from certbot import le_util +from certbot import util from certbot import hooks from certbot import storage from certbot.plugins import disco as plugins_disco @@ -86,7 +86,7 @@ def _reconstitute(config, full_path): return None try: - config.domains = [le_util.enforce_domain_sanity(d) + config.domains = [util.enforce_domain_sanity(d) for d in renewal_candidate.names()] except errors.ConfigurationError as error: logger.warning("Renewal configuration file %s references a cert " diff --git a/certbot/reporter.py b/certbot/reporter.py index d509cb0b8..0c5238d12 100644 --- a/certbot/reporter.py +++ b/certbot/reporter.py @@ -11,7 +11,7 @@ from six.moves import queue # pylint: disable=import-error import zope.interface from certbot import interfaces -from certbot import le_util +from certbot import util logger = logging.getLogger(__name__) @@ -79,7 +79,7 @@ class Reporter(object): bold_on = sys.stdout.isatty() if not self.config.quiet: if bold_on: - print(le_util.ANSI_SGR_BOLD) + print(util.ANSI_SGR_BOLD) print('IMPORTANT NOTES:') first_wrapper = textwrap.TextWrapper( initial_indent=' - ', subsequent_indent=(' ' * 3)) @@ -96,7 +96,7 @@ class Reporter(object): if no_exception or msg.on_crash: if bold_on and msg.priority > self.HIGH_PRIORITY: if not self.config.quiet: - sys.stdout.write(le_util.ANSI_SGR_RESET) + sys.stdout.write(util.ANSI_SGR_RESET) bold_on = False lines = msg.text.splitlines() print(first_wrapper.fill(lines[0])) @@ -104,4 +104,4 @@ class Reporter(object): print("\n".join( next_wrapper.fill(line) for line in lines[1:])) if bold_on and not self.config.quiet: - sys.stdout.write(le_util.ANSI_SGR_RESET) + sys.stdout.write(util.ANSI_SGR_RESET) diff --git a/certbot/reverter.py b/certbot/reverter.py index 16ee5d8a4..f8140d60d 100644 --- a/certbot/reverter.py +++ b/certbot/reverter.py @@ -13,7 +13,7 @@ import zope.component from certbot import constants from certbot import errors from certbot import interfaces -from certbot import le_util +from certbot import util from certbot.display import util as display_util @@ -33,7 +33,7 @@ class Reverter(object): def __init__(self, config): self.config = config - le_util.make_or_verify_dir( + util.make_or_verify_dir( config.backup_dir, constants.CONFIG_DIRS_MODE, os.geteuid(), self.config.strict_permissions) @@ -185,7 +185,7 @@ class Reverter(object): :raises .ReverterError: if unable to add checkpoint """ - le_util.make_or_verify_dir( + util.make_or_verify_dir( cp_dir, constants.CONFIG_DIRS_MODE, os.geteuid(), self.config.strict_permissions) @@ -281,7 +281,7 @@ class Reverter(object): csvreader = csv.reader(csvfile) for command in reversed(list(csvreader)): try: - le_util.run_script(command) + util.run_script(command) except errors.SubprocessError: logger.error( "Unable to run undo command: %s", " ".join(command)) @@ -397,7 +397,7 @@ class Reverter(object): else: cp_dir = self.config.in_progress_dir - le_util.make_or_verify_dir( + util.make_or_verify_dir( cp_dir, constants.CONFIG_DIRS_MODE, os.geteuid(), self.config.strict_permissions) diff --git a/certbot/storage.py b/certbot/storage.py index 6c13eb844..b0c8245d3 100644 --- a/certbot/storage.py +++ b/certbot/storage.py @@ -13,12 +13,12 @@ from certbot import constants from certbot import crypto_util from certbot import errors from certbot import error_handler -from certbot import le_util +from certbot import util logger = logging.getLogger(__name__) ALL_FOUR = ("cert", "privkey", "chain", "fullchain") -CURRENT_VERSION = le_util.get_strict_version(certbot.__version__) +CURRENT_VERSION = util.get_strict_version(certbot.__version__) def config_with_defaults(config=None): @@ -264,7 +264,7 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes conf_version = self.configuration.get("version") if (conf_version is not None and - le_util.get_strict_version(conf_version) > CURRENT_VERSION): + util.get_strict_version(conf_version) > CURRENT_VERSION): logger.warning( "Attempting to parse the version %s renewal configuration " "file found at %s with version %s of Certbot. This might not " @@ -769,7 +769,7 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes if not os.path.exists(i): os.makedirs(i, 0o700) logger.debug("Creating directory %s.", i) - config_file, config_filename = le_util.unique_lineage_name( + config_file, config_filename = util.unique_lineage_name( cli_config.renewal_configs_dir, lineagename) if not config_filename.endswith(".conf"): raise errors.CertStorageError( diff --git a/certbot/tests/auth_handler_test.py b/certbot/tests/auth_handler_test.py index 3facd4f7c..eccc36418 100644 --- a/certbot/tests/auth_handler_test.py +++ b/certbot/tests/auth_handler_test.py @@ -11,7 +11,7 @@ from acme import messages from certbot import achallenges from certbot import errors -from certbot import le_util +from certbot import util from certbot.tests import acme_util @@ -69,7 +69,7 @@ class GetAuthorizationsTest(unittest.TestCase): self.mock_auth.perform.side_effect = gen_auth_resp - self.mock_account = mock.Mock(key=le_util.Key("file_path", "PEM")) + self.mock_account = mock.Mock(key=util.Key("file_path", "PEM")) self.mock_net = mock.MagicMock(spec=acme_client.Client) self.handler = AuthHandler( diff --git a/certbot/tests/cli_test.py b/certbot/tests/cli_test.py index 00c9a0a26..0beff81e7 100644 --- a/certbot/tests/cli_test.py +++ b/certbot/tests/cli_test.py @@ -22,7 +22,7 @@ from certbot import configuration from certbot import constants from certbot import crypto_util from certbot import errors -from certbot import le_util +from certbot import util from certbot import main from certbot import renewal from certbot import storage @@ -163,7 +163,7 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods with mock.patch('certbot.main.client.acme_client.ClientNetwork') as acme_net: self._call_no_clientmock(args) - os_ver = " ".join(le_util.get_os_info()) + os_ver = " ".join(util.get_os_info()) ua = acme_net.call_args[1]["user_agent"] self.assertTrue(os_ver in ua) import platform @@ -201,7 +201,7 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods '--key-path', 'key', '--chain-path', 'chain']) self.assertEqual(mock_pick_installer.call_count, 1) - @mock.patch('certbot.le_util.exe_exists') + @mock.patch('certbot.util.exe_exists') def test_configurator_selection(self, mock_exe_exists): mock_exe_exists.return_value = True real_plugins = disco.PluginsRegistry.find_all() @@ -983,7 +983,7 @@ class DuplicativeCertsTest(storage_test.BaseRenewableCertTest): def tearDown(self): shutil.rmtree(self.tempdir) - @mock.patch('certbot.le_util.make_or_verify_dir') + @mock.patch('certbot.util.make_or_verify_dir') def test_find_duplicative_names(self, unused_makedir): from certbot.main import _find_duplicative_certs test_cert = test_util.load_vector('cert-san.pem') diff --git a/certbot/tests/client_test.py b/certbot/tests/client_test.py index 8490efd9f..9156277a9 100644 --- a/certbot/tests/client_test.py +++ b/certbot/tests/client_test.py @@ -11,7 +11,7 @@ from acme import jose from certbot import account from certbot import errors -from certbot import le_util +from certbot import util from certbot.tests import test_util @@ -137,7 +137,7 @@ class ClientTest(unittest.TestCase): @mock.patch("certbot.client.logger") def test_obtain_certificate_from_csr(self, mock_logger): self._mock_obtain_certificate() - test_csr = le_util.CSR(form="der", file=None, data=CSR_SAN) + test_csr = util.CSR(form="der", file=None, data=CSR_SAN) auth_handler = self.client.auth_handler authzr = auth_handler.get_authorizations(self.eg_domains, False) @@ -172,7 +172,7 @@ class ClientTest(unittest.TestCase): def test_obtain_certificate(self, mock_crypto_util): self._mock_obtain_certificate() - csr = le_util.CSR(form="der", file=None, data=CSR_SAN) + csr = util.CSR(form="der", file=None, data=CSR_SAN) mock_crypto_util.init_save_csr.return_value = csr mock_crypto_util.init_save_key.return_value = mock.sentinel.key domains = ["example.com", "www.example.com"] diff --git a/certbot/tests/colored_logging_test.py b/certbot/tests/colored_logging_test.py index 91c6b8c08..0a7929561 100644 --- a/certbot/tests/colored_logging_test.py +++ b/certbot/tests/colored_logging_test.py @@ -4,7 +4,7 @@ import unittest import six -from certbot import le_util +from certbot import util class StreamHandlerTest(unittest.TestCase): @@ -32,9 +32,9 @@ class StreamHandlerTest(unittest.TestCase): self.logger.debug(msg) self.assertEqual(self.stream.getvalue(), - '{0}{1}{2}\n'.format(le_util.ANSI_SGR_RED, + '{0}{1}{2}\n'.format(util.ANSI_SGR_RED, msg, - le_util.ANSI_SGR_RESET)) + util.ANSI_SGR_RESET)) if __name__ == "__main__": diff --git a/certbot/tests/crypto_util_test.py b/certbot/tests/crypto_util_test.py index eeea0f4ab..fa88e89e7 100644 --- a/certbot/tests/crypto_util_test.py +++ b/certbot/tests/crypto_util_test.py @@ -10,7 +10,7 @@ import zope.component from certbot import errors from certbot import interfaces -from certbot import le_util +from certbot import util from certbot.tests import test_util @@ -63,7 +63,7 @@ class InitSaveCSRTest(unittest.TestCase): shutil.rmtree(self.csr_dir) @mock.patch('certbot.crypto_util.make_csr') - @mock.patch('certbot.crypto_util.le_util.make_or_verify_dir') + @mock.patch('certbot.crypto_util.util.make_or_verify_dir') def test_it(self, unused_mock_verify, mock_csr): from certbot.crypto_util import init_save_csr @@ -174,9 +174,9 @@ class ImportCSRFileTest(unittest.TestCase): self.assertEqual( (OpenSSL.crypto.FILETYPE_ASN1, - le_util.CSR(file=csrfile, - data=data, - form="der"), + util.CSR(file=csrfile, + data=data, + form="der"), ["example.com"],), self._call(csrfile, data)) @@ -186,9 +186,9 @@ class ImportCSRFileTest(unittest.TestCase): self.assertEqual( (OpenSSL.crypto.FILETYPE_PEM, - le_util.CSR(file=csrfile, - data=data, - form="pem"), + util.CSR(file=csrfile, + data=data, + form="pem"), ["example.com"],), self._call(csrfile, data)) diff --git a/certbot/tests/display/ops_test.py b/certbot/tests/display/ops_test.py index 05cb6b12d..874a9cc9e 100644 --- a/certbot/tests/display/ops_test.py +++ b/certbot/tests/display/ops_test.py @@ -41,13 +41,13 @@ class GetEmailTest(unittest.TestCase): def test_ok_safe(self): self.input.return_value = (display_util.OK, "foo@bar.baz") - with mock.patch("certbot.display.ops.le_util.safe_email") as mock_safe_email: + with mock.patch("certbot.display.ops.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("certbot.display.ops.le_util.safe_email") as mock_safe_email: + with mock.patch("certbot.display.ops.util.safe_email") as mock_safe_email: mock_safe_email.side_effect = [False, True] self.assertTrue(self._call() is "foo@bar.baz") @@ -56,7 +56,7 @@ class GetEmailTest(unittest.TestCase): invalid_txt = "There seem to be problems" base_txt = "Enter email" self.input.return_value = (display_util.OK, "foo@bar.baz") - with mock.patch("certbot.display.ops.le_util.safe_email") as mock_safe_email: + with mock.patch("certbot.display.ops.util.safe_email") as mock_safe_email: mock_safe_email.return_value = True self._call() msg = self.input.call_args[0][0] diff --git a/certbot/tests/reverter_test.py b/certbot/tests/reverter_test.py index 85234b76a..58cc68dce 100644 --- a/certbot/tests/reverter_test.py +++ b/certbot/tests/reverter_test.py @@ -164,7 +164,7 @@ class ReverterCheckpointLocalTest(unittest.TestCase): errors.ReverterError, self.reverter.register_undo_command, True, ["command"]) - @mock.patch("certbot.le_util.run_script") + @mock.patch("certbot.util.run_script") def test_run_undo_commands(self, mock_run): mock_run.side_effect = ["", errors.SubprocessError] coms = [ diff --git a/certbot/tests/storage_test.py b/certbot/tests/storage_test.py index f19b7d89d..0c88d3d55 100644 --- a/certbot/tests/storage_test.py +++ b/certbot/tests/storage_test.py @@ -682,7 +682,7 @@ class RenewableCertTests(BaseRenewableCertTest): self.assertTrue(os.path.exists(os.path.join( self.cli_config.archive_dir, "the-lineage.com", "privkey1.pem"))) - @mock.patch("certbot.storage.le_util.unique_lineage_name") + @mock.patch("certbot.storage.util.unique_lineage_name") def test_invalid_config_filename(self, mock_uln): from certbot import storage mock_uln.return_value = "this_does_not_end_with_dot_conf", "yikes" diff --git a/certbot/tests/le_util_test.py b/certbot/tests/util_test.py similarity index 85% rename from certbot/tests/le_util_test.py rename to certbot/tests/util_test.py index 6e4eef0f1..ada64edb2 100644 --- a/certbot/tests/le_util_test.py +++ b/certbot/tests/util_test.py @@ -1,4 +1,4 @@ -"""Tests for certbot.le_util.""" +"""Tests for certbot.util.""" import argparse import errno import os @@ -15,13 +15,13 @@ from certbot import errors class RunScriptTest(unittest.TestCase): - """Tests for certbot.le_util.run_script.""" + """Tests for certbot.util.run_script.""" @classmethod def _call(cls, params): - from certbot.le_util import run_script + from certbot.util import run_script return run_script(params) - @mock.patch("certbot.le_util.subprocess.Popen") + @mock.patch("certbot.util.subprocess.Popen") def test_default(self, mock_popen): """These will be changed soon enough with reload.""" mock_popen().returncode = 0 @@ -31,13 +31,13 @@ class RunScriptTest(unittest.TestCase): self.assertEqual(out, "stdout") self.assertEqual(err, "stderr") - @mock.patch("certbot.le_util.subprocess.Popen") + @mock.patch("certbot.util.subprocess.Popen") def test_bad_process(self, mock_popen): mock_popen.side_effect = OSError self.assertRaises(errors.SubprocessError, self._call, ["test"]) - @mock.patch("certbot.le_util.subprocess.Popen") + @mock.patch("certbot.util.subprocess.Popen") def test_failure(self, mock_popen): mock_popen().communicate.return_value = ("", "") mock_popen().returncode = 1 @@ -46,29 +46,29 @@ class RunScriptTest(unittest.TestCase): class ExeExistsTest(unittest.TestCase): - """Tests for certbot.le_util.exe_exists.""" + """Tests for certbot.util.exe_exists.""" @classmethod def _call(cls, exe): - from certbot.le_util import exe_exists + from certbot.util import exe_exists return exe_exists(exe) - @mock.patch("certbot.le_util.os.path.isfile") - @mock.patch("certbot.le_util.os.access") + @mock.patch("certbot.util.os.path.isfile") + @mock.patch("certbot.util.os.access") def test_full_path(self, mock_access, mock_isfile): mock_access.return_value = True mock_isfile.return_value = True self.assertTrue(self._call("/path/to/exe")) - @mock.patch("certbot.le_util.os.path.isfile") - @mock.patch("certbot.le_util.os.access") + @mock.patch("certbot.util.os.path.isfile") + @mock.patch("certbot.util.os.access") def test_on_path(self, mock_access, mock_isfile): mock_access.return_value = True mock_isfile.return_value = True self.assertTrue(self._call("exe")) - @mock.patch("certbot.le_util.os.path.isfile") - @mock.patch("certbot.le_util.os.access") + @mock.patch("certbot.util.os.path.isfile") + @mock.patch("certbot.util.os.access") def test_not_found(self, mock_access, mock_isfile): mock_access.return_value = False mock_isfile.return_value = True @@ -76,7 +76,7 @@ class ExeExistsTest(unittest.TestCase): class MakeOrVerifyDirTest(unittest.TestCase): - """Tests for certbot.le_util.make_or_verify_dir. + """Tests for certbot.util.make_or_verify_dir. Note that it is not possible to test for a wrong directory owner, as this testing script would have to be run as root. @@ -94,7 +94,7 @@ class MakeOrVerifyDirTest(unittest.TestCase): shutil.rmtree(self.root_path, ignore_errors=True) def _call(self, directory, mode): - from certbot.le_util import make_or_verify_dir + from certbot.util import make_or_verify_dir return make_or_verify_dir(directory, mode, self.uid, strict=True) def test_creates_dir_when_missing(self): @@ -117,7 +117,7 @@ class MakeOrVerifyDirTest(unittest.TestCase): class CheckPermissionsTest(unittest.TestCase): - """Tests for certbot.le_util.check_permissions. + """Tests for certbot.util.check_permissions. Note that it is not possible to test for a wrong file owner, as this testing script would have to be run as root. @@ -132,7 +132,7 @@ class CheckPermissionsTest(unittest.TestCase): os.remove(self.path) def _call(self, mode): - from certbot.le_util import check_permissions + from certbot.util import check_permissions return check_permissions(self.path, mode, self.uid) def test_ok_mode(self): @@ -145,7 +145,7 @@ class CheckPermissionsTest(unittest.TestCase): class UniqueFileTest(unittest.TestCase): - """Tests for certbot.le_util.unique_file.""" + """Tests for certbot.util.unique_file.""" def setUp(self): self.root_path = tempfile.mkdtemp() @@ -155,7 +155,7 @@ class UniqueFileTest(unittest.TestCase): shutil.rmtree(self.root_path, ignore_errors=True) def _call(self, mode=0o600): - from certbot.le_util import unique_file + from certbot.util import unique_file return unique_file(self.default_name, mode) def test_returns_fd_for_writing(self): @@ -190,7 +190,7 @@ class UniqueFileTest(unittest.TestCase): class UniqueLineageNameTest(unittest.TestCase): - """Tests for certbot.le_util.unique_lineage_name.""" + """Tests for certbot.util.unique_lineage_name.""" def setUp(self): self.root_path = tempfile.mkdtemp() @@ -199,7 +199,7 @@ class UniqueLineageNameTest(unittest.TestCase): shutil.rmtree(self.root_path, ignore_errors=True) def _call(self, filename, mode=0o777): - from certbot.le_util import unique_lineage_name + from certbot.util import unique_lineage_name return unique_lineage_name(self.root_path, filename, mode) def test_basic(self): @@ -214,14 +214,14 @@ class UniqueLineageNameTest(unittest.TestCase): self.assertTrue(isinstance(name, str)) self.assertTrue("wow-0009.conf" in name) - @mock.patch("certbot.le_util.os.fdopen") + @mock.patch("certbot.util.os.fdopen") def test_failure(self, mock_fdopen): err = OSError("whoops") err.errno = errno.EIO mock_fdopen.side_effect = err self.assertRaises(OSError, self._call, "wow") - @mock.patch("certbot.le_util.os.fdopen") + @mock.patch("certbot.util.os.fdopen") def test_subsequent_failure(self, mock_fdopen): self._call("wow") err = OSError("whoops") @@ -231,7 +231,7 @@ class UniqueLineageNameTest(unittest.TestCase): class SafelyRemoveTest(unittest.TestCase): - """Tests for certbot.le_util.safely_remove.""" + """Tests for certbot.util.safely_remove.""" def setUp(self): self.tmp = tempfile.mkdtemp() @@ -241,7 +241,7 @@ class SafelyRemoveTest(unittest.TestCase): shutil.rmtree(self.tmp) def _call(self): - from certbot.le_util import safely_remove + from certbot.util import safely_remove return safely_remove(self.path) def test_exists(self): @@ -255,7 +255,7 @@ class SafelyRemoveTest(unittest.TestCase): # no error, yay! self.assertFalse(os.path.exists(self.path)) - @mock.patch("certbot.le_util.os.remove") + @mock.patch("certbot.util.os.remove") def test_other_error_passthrough(self, mock_remove): mock_remove.side_effect = OSError self.assertRaises(OSError, self._call) @@ -265,7 +265,7 @@ class SafeEmailTest(unittest.TestCase): """Test safe_email.""" @classmethod def _call(cls, addr): - from certbot.le_util import safe_email + from certbot.util import safe_email return safe_email(addr) def test_valid_emails(self): @@ -293,7 +293,7 @@ class AddDeprecatedArgumentTest(unittest.TestCase): self.parser = argparse.ArgumentParser() def _call(self, argument_name, nargs): - from certbot.le_util import add_deprecated_argument + from certbot.util import add_deprecated_argument add_deprecated_argument(self.parser.add_argument, argument_name, nargs) @@ -309,14 +309,14 @@ class AddDeprecatedArgumentTest(unittest.TestCase): def _get_argparse_warnings(self, args): stderr = six.StringIO() - with mock.patch("certbot.le_util.sys.stderr", new=stderr): + with mock.patch("certbot.util.sys.stderr", new=stderr): self.parser.parse_args(args) return stderr.getvalue() def test_help(self): self._call("--old-option", 2) stdout = six.StringIO() - with mock.patch("certbot.le_util.sys.stdout", new=stdout): + with mock.patch("certbot.util.sys.stdout", new=stdout): try: self.parser.parse_args(["-h"]) except SystemExit: @@ -328,7 +328,7 @@ class EnforceDomainSanityTest(unittest.TestCase): """Test enforce_domain_sanity.""" def _call(self, domain): - from certbot.le_util import enforce_domain_sanity + from certbot.util import enforce_domain_sanity return enforce_domain_sanity(domain) def test_nonascii_str(self): @@ -341,11 +341,11 @@ class EnforceDomainSanityTest(unittest.TestCase): class GetStrictVersionTest(unittest.TestCase): - """Tests for certbot.le_util.get_strict_version.""" + """Tests for certbot.util.get_strict_version.""" @classmethod def _call(cls, *args, **kwargs): - from certbot.le_util import get_strict_version + from certbot.util import get_strict_version return get_strict_version(*args, **kwargs) def test_two_dev_versions(self): diff --git a/certbot/le_util.py b/certbot/util.py similarity index 100% rename from certbot/le_util.py rename to certbot/util.py From 5b2ab14711708099f16743801ebbac15bd31feaf Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 25 May 2016 16:50:18 -0700 Subject: [PATCH 221/396] Revert "Pin old pkginfo version" This reverts commit 4919814dd17bdb8a7b0100ac89a5196cb4766310. --- setup.py | 1 - 1 file changed, 1 deletion(-) diff --git a/setup.py b/setup.py index 59da23de4..4ee56576b 100644 --- a/setup.py +++ b/setup.py @@ -71,7 +71,6 @@ dev_extras = [ 'nose', 'nosexcover', 'pep8', - 'pkginfo<=1.2.1', 'pylint==1.4.2', # upstream #248 'tox', 'twine', From 6598bcb53eda7e16975e6129f3700bd1944270db Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 25 May 2016 21:53:20 -0700 Subject: [PATCH 222/396] refactor get_email --- certbot/client.py | 2 +- certbot/display/ops.py | 69 +++++++++++++++++++------------ certbot/tests/display/ops_test.py | 15 +++---- 3 files changed, 49 insertions(+), 37 deletions(-) diff --git a/certbot/client.py b/certbot/client.py index ba31f8760..a885d0426 100644 --- a/certbot/client.py +++ b/certbot/client.py @@ -150,7 +150,7 @@ def perform_registration(acme, config): return acme.register(messages.NewRegistration.from_data(email=config.email)) except messages.Error as e: if e.typ == "urn:acme:error:invalidEmail": - config.namespace.email = display_ops.get_email(more=True, invalid=True) + config.namespace.email = display_ops.get_email(invalid=True) return perform_registration(acme, config) else: raise diff --git a/certbot/display/ops.py b/certbot/display/ops.py index 6752bf0c1..16c44a881 100644 --- a/certbot/display/ops.py +++ b/certbot/display/ops.py @@ -15,41 +15,56 @@ logger = logging.getLogger(__name__) z_util = zope.component.getUtility -def get_email(more=False, invalid=False): +def get_email(invalid=False, optional=True): """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 + :param bool invalid: True if an invalid was provided by the user + :param bool optional: True if the user can use + --register-unsafely-without-email to avoid providing an e-mail - :returns: Email or ``None`` if cancelled by user. + :returns: e-mail address :rtype: str - """ - 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') - try: - code, email = zope.component.getUtility(interfaces.IDisplay).input(msg) - except errors.MissingCommandlineFlag: - msg = ("You should register before running non-interactively, or provide --agree-tos" - " and --email flags") - raise errors.MissingCommandlineFlag(msg) + :raises errors.Error: if the user cancels - if code == display_util.OK: - if le_util.safe_email(email): - return email + """ + invalid_prefix = "There seem to be problems with that address. " + msg = "Enter email address (used for urgent notices and lost key recovery)" + unsafe_suggestion = ("\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") + if optional: + if invalid: + msg += unsafe_suggestion else: - # 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 != "")) + suggest_unsafe = True else: - return None + suggest_unsafe = False + + while True: + try: + code, email = z_util(interfaces.IDisplay).input( + invalid_prefix + msg if invalid else msg) + except errors.MissingCommandlineFlag: + msg = ("You should register before running non-interactively, " + "or provide --agree-tos and --email flags") + raise errors.MissingCommandlineFlag(msg) + + if code != display_util.OK: + if optional: + raise errors.Error( + "An e-mail address or " + "--register-unsafely-without-email must be provided.") + else: + raise errors.Error("An e-mail address must be provided.") + elif le_util.safe_email(email): + return email + elif suggest_unsafe: + msg += unsafe_suggestion + suggest_unsafe = False # add this message at most once + + invalid = bool(email) def choose_account(accounts): diff --git a/certbot/tests/display/ops_test.py b/certbot/tests/display/ops_test.py index 05cb6b12d..be496a42a 100644 --- a/certbot/tests/display/ops_test.py +++ b/certbot/tests/display/ops_test.py @@ -12,6 +12,7 @@ from acme import jose from acme import messages from certbot import account +from certbot import errors from certbot import interfaces from certbot.display import util as display_util @@ -37,7 +38,7 @@ class GetEmailTest(unittest.TestCase): def test_cancel_none(self): self.input.return_value = (display_util.CANCEL, "foo@bar.baz") - self.assertTrue(self._call() is None) + self.assertRaises(errors.Error, self._call) def test_ok_safe(self): self.input.return_value = (display_util.OK, "foo@bar.baz") @@ -52,7 +53,7 @@ class GetEmailTest(unittest.TestCase): self.assertTrue(self._call() is "foo@bar.baz") def test_more_and_invalid_flags(self): - more_txt = "--register-unsafely-without-email" + optional_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") @@ -60,16 +61,12 @@ class GetEmailTest(unittest.TestCase): mock_safe_email.return_value = True self._call() msg = self.input.call_args[0][0] - self.assertTrue(more_txt not in msg) + self.assertTrue(optional_txt not in msg) self.assertTrue(invalid_txt not in msg) self.assertTrue(base_txt in msg) - self._call(more=True) + self._call(invalid=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(optional_txt in msg) self.assertTrue(invalid_txt in msg) self.assertTrue(base_txt in msg) From 8d802ef6fd3699e4ee1f695cdf985336606fe9fc Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 25 May 2016 22:12:06 -0700 Subject: [PATCH 223/396] Fix up display/ops.py coverage --- certbot/tests/display/ops_test.py | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/certbot/tests/display/ops_test.py b/certbot/tests/display/ops_test.py index be496a42a..84a4ada3f 100644 --- a/certbot/tests/display/ops_test.py +++ b/certbot/tests/display/ops_test.py @@ -39,6 +39,7 @@ class GetEmailTest(unittest.TestCase): def test_cancel_none(self): self.input.return_value = (display_util.CANCEL, "foo@bar.baz") self.assertRaises(errors.Error, self._call) + self.assertRaises(errors.Error, self._call, optional=False) def test_ok_safe(self): self.input.return_value = (display_util.OK, "foo@bar.baz") @@ -52,23 +53,24 @@ class GetEmailTest(unittest.TestCase): mock_safe_email.side_effect = [False, True] self.assertTrue(self._call() is "foo@bar.baz") - def test_more_and_invalid_flags(self): - optional_txt = "--register-unsafely-without-email" + def test_invalid_flag(self): invalid_txt = "There seem to be problems" - base_txt = "Enter email" self.input.return_value = (display_util.OK, "foo@bar.baz") with mock.patch("certbot.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(optional_txt not in msg) - self.assertTrue(invalid_txt not in msg) - self.assertTrue(base_txt in msg) + self.assertTrue(invalid_txt not in self.input.call_args[0][0]) self._call(invalid=True) - msg = self.input.call_args[0][0] - self.assertTrue(optional_txt in msg) - self.assertTrue(invalid_txt in msg) - self.assertTrue(base_txt in msg) + self.assertTrue(invalid_txt in self.input.call_args[0][0]) + + def test_optional_flag(self): + self.input.return_value = (display_util.OK, "foo@bar.baz") + with mock.patch("certbot.display.ops.le_util.safe_email") as mock_safe_email: + mock_safe_email.side_effect = [False, True] + self._call(optional=False) + for call in self.input.call_args_list: + self.assertTrue( + "--register-unsafely-without-email" not in call[0][0]) class ChooseAccountTest(unittest.TestCase): From 89279a72bd03c5f05b8b00813e7ea3801059aa4d Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 25 May 2016 22:23:36 -0700 Subject: [PATCH 224/396] Interatively ask for e-mail with register verb --- certbot/main.py | 9 ++++++--- certbot/tests/cli_test.py | 11 ++++++----- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/certbot/main.py b/certbot/main.py index 20b9a7ce5..c048d9277 100644 --- a/certbot/main.py +++ b/certbot/main.py @@ -391,9 +391,12 @@ def register(config, unused_plugins): if len(accounts) == 0: return "Could not find an existing account to update." if config.email is None: - return ("Currently, --update-registration can only change the e-mail " - "address\nassociated with an account. A new e-mail address is " - "required\n(hint: --email)") + if config.register_unsafely_without_email: + return ("--register-unsafely-without-email provided, however, a " + "new e-mail address must\ncurrently be provided when " + "updating a registration.") + config.namespace.email = display_ops.get_email(optional=False) + acc, acme = _determine_account(config) acme_client = client.Client(config, acc, None, None, acme=acme) # We rely on an exception to interrupt this process if it didn't work. diff --git a/certbot/tests/cli_test.py b/certbot/tests/cli_test.py index b348e8ea7..b6103c2e8 100644 --- a/certbot/tests/cli_test.py +++ b/certbot/tests/cli_test.py @@ -942,16 +942,17 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods "user@example.org"]) self.assertTrue("Could not find an existing account" in x[0]) - def test_update_registration_no_email(self): + def test_update_registration_unsafely(self): # This test will become obsolete when register --update-registration - # supports updating something other than the e-mail address! - # with mock.patch('certbot.main.client') as mocked_client: + # supports removing an e-mail address from the account with mock.patch('certbot.main.account') as mocked_account: mocked_storage = mock.MagicMock() mocked_account.AccountFileStorage.return_value = mocked_storage mocked_storage.find_all.return_value = ["an account"] - x = self._call_no_clientmock(["register", "--update-registration"]) - self.assertTrue("can only change the e-mail" in x[0]) + x = self._call_no_clientmock( + "register --update-registration " + "--register-unsafely-without-email".split()) + self.assertTrue("--register-unsafely-without-email" in x[0]) def test_update_registration_with_email(self): with mock.patch('certbot.main.client') as mocked_client: From 14b45b795a0a37af9b039a10728d290b6a113874 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 25 May 2016 22:45:57 -0700 Subject: [PATCH 225/396] give register full test coverage --- certbot/tests/cli_test.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/certbot/tests/cli_test.py b/certbot/tests/cli_test.py index b6103c2e8..0bf34eb10 100644 --- a/certbot/tests/cli_test.py +++ b/certbot/tests/cli_test.py @@ -954,7 +954,11 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods "--register-unsafely-without-email".split()) self.assertTrue("--register-unsafely-without-email" in x[0]) - def test_update_registration_with_email(self): + @mock.patch('certbot.main.display_ops.get_email') + @mock.patch('certbot.main.zope.component.getUtility') + def test_update_registration_with_email(self, mock_utility, mock_email): + email = "user@example.com" + mock_email.return_value = email with mock.patch('certbot.main.client') as mocked_client: with mock.patch('certbot.main.account') as mocked_account: with mock.patch('certbot.main._determine_account') as mocked_det: @@ -966,8 +970,7 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods acme_client = mock.MagicMock() mocked_client.Client.return_value = acme_client x = self._call_no_clientmock( - ["register", "--update-registration", "--email", - "user@example.org"]) + ["register", "--update-registration"]) # When registration change succeeds, the return value # of register() is None self.assertTrue(x[0] is None) @@ -977,6 +980,8 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods acme_client.acme.update_registration.called) # and we saved the updated registration on disk self.assertTrue(mocked_storage.save_regr.called) + self.assertTrue( + email in mock_utility().add_message.call_args[0][0]) def test_conflicting_args(self): args = ['renew', '--dialog', '--text'] From 1322ae12ce2bcd362399023ee2852927f7910a1b Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Thu, 26 May 2016 10:20:47 -0700 Subject: [PATCH 226/396] Stop packaging letshelp --- tools/release.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/release.sh b/tools/release.sh index abaad09ff..ca09c03a4 100755 --- a/tools/release.sh +++ b/tools/release.sh @@ -45,7 +45,7 @@ export GPG_TTY=$(tty) PORT=${PORT:-1234} # subpackages to be released -SUBPKGS=${SUBPKGS:-"acme certbot-apache certbot-nginx letshelp-certbot letsencrypt letsencrypt-apache letsencrypt-nginx letshelp-letsencrypt"} +SUBPKGS=${SUBPKGS:-"acme certbot-apache certbot-nginx letsencrypt letsencrypt-apache letsencrypt-nginx"} subpkgs_modules="$(echo $SUBPKGS | sed s/-/_/g)" # certbot_compatibility_test is not packaged because: # - it is not meant to be used by anyone else than Certbot devs From 7e039d15044738bca2326f26e8e7bdc2b88386db Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Thu, 26 May 2016 10:24:57 -0700 Subject: [PATCH 227/396] With us packaging the shim packages, there are more lines in letsencrypt-auto-requirements.txt that will change with every release. This change strips the hashes of the previous packages before adding the new ones. --- tools/release.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/release.sh b/tools/release.sh index ca09c03a4..154172322 100755 --- a/tools/release.sh +++ b/tools/release.sh @@ -176,7 +176,7 @@ if ! wc -l /tmp/hashes.$$ | grep -qE "^\s*15 " ; then fi # perform hideous surgery on requirements.txt... -head -n -9 letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt > /tmp/req.$$ +head -n -15 letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt > /tmp/req.$$ cat /tmp/hashes.$$ >> /tmp/req.$$ cp /tmp/req.$$ letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt From a7edc4b1e5ac9f99875ca7bc887d6db4b3415c97 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Thu, 26 May 2016 10:33:18 -0700 Subject: [PATCH 228/396] Previously, the script relied on global `pip` for hashing packages. This doesn't work if you don't have `pip` installed (like me) and I think using `pip` from the venv should be preferred to ensure you are using the latest `pip` (which was updated in the venv earlier in the script). --- tools/release.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/release.sh b/tools/release.sh index 154172322..89a2f5140 100755 --- a/tools/release.sh +++ b/tools/release.sh @@ -162,13 +162,13 @@ for module in certbot $subpkgs_modules ; do echo testing $module nosetests $module done -deactivate # pin pip hashes of the things we just built for pkg in acme certbot certbot-apache letsencrypt letsencrypt-apache ; do echo $pkg==$version \\ pip hash dist."$version/$pkg"/*.{whl,gz} | grep "^--hash" | python2 -c 'from sys import stdin; input = stdin.read(); print " ", input.replace("\n--hash", " \\\n --hash"),' done > /tmp/hashes.$$ +deactivate if ! wc -l /tmp/hashes.$$ | grep -qE "^\s*15 " ; then echo Unexpected pip hash output From f0dc0de40a50c6b6957691a8c43fc51598f05a04 Mon Sep 17 00:00:00 2001 From: Blake Griffith Date: Thu, 26 May 2016 13:43:00 -0500 Subject: [PATCH 229/396] Catch more le_util usage in certbot-apache --- .../certbot_apache/tests/configurator_test.py | 48 +++++++++---------- .../certbot_apache/tests/constants_test.py | 6 +-- .../certbot_apache/tests/tls_sni_01_test.py | 4 +- certbot-apache/certbot_apache/tests/util.py | 4 +- 4 files changed, 31 insertions(+), 31 deletions(-) diff --git a/certbot-apache/certbot_apache/tests/configurator_test.py b/certbot-apache/certbot_apache/tests/configurator_test.py index d7a5fd75a..a2e39de47 100644 --- a/certbot-apache/certbot_apache/tests/configurator_test.py +++ b/certbot-apache/certbot_apache/tests/configurator_test.py @@ -49,14 +49,14 @@ class MultipleVhostsTest(util.ApacheTest): shutil.rmtree(self.config_dir) shutil.rmtree(self.work_dir) - @mock.patch("certbot_apache.configurator.le_util.exe_exists") + @mock.patch("certbot_apache.configurator.util.exe_exists") def test_prepare_no_install(self, mock_exe_exists): mock_exe_exists.return_value = False self.assertRaises( errors.NoInstallationError, self.config.prepare) @mock.patch("certbot_apache.parser.ApacheParser") - @mock.patch("certbot_apache.configurator.le_util.exe_exists") + @mock.patch("certbot_apache.configurator.util.exe_exists") def test_prepare_version(self, mock_exe_exists, _): mock_exe_exists.return_value = True self.config.version = None @@ -67,7 +67,7 @@ class MultipleVhostsTest(util.ApacheTest): errors.NotSupportedError, self.config.prepare) @mock.patch("certbot_apache.parser.ApacheParser") - @mock.patch("certbot_apache.configurator.le_util.exe_exists") + @mock.patch("certbot_apache.configurator.util.exe_exists") def test_prepare_old_aug(self, mock_exe_exists, _): mock_exe_exists.return_value = True self.config.config_test = mock.Mock() @@ -268,8 +268,8 @@ class MultipleVhostsTest(util.ApacheTest): self.config.is_site_enabled, "irrelevant") - @mock.patch("certbot.le_util.run_script") - @mock.patch("certbot.le_util.exe_exists") + @mock.patch("certbot.util.run_script") + @mock.patch("certbot.util.exe_exists") @mock.patch("certbot_apache.parser.subprocess.Popen") def test_enable_mod(self, mock_popen, mock_exe_exists, mock_run_script): mock_popen().communicate.return_value = ("Define: DUMP_RUN_CFG", "") @@ -287,7 +287,7 @@ class MultipleVhostsTest(util.ApacheTest): self.assertRaises( errors.NotSupportedError, self.config.enable_mod, "ssl") - @mock.patch("certbot.le_util.exe_exists") + @mock.patch("certbot.util.exe_exists") def test_enable_mod_no_disable(self, mock_exe_exists): mock_exe_exists.return_value = False self.assertRaises( @@ -695,7 +695,7 @@ class MultipleVhostsTest(util.ApacheTest): self.config.cleanup([achall1, achall2]) self.assertTrue(mock_restart.called) - @mock.patch("certbot.le_util.run_script") + @mock.patch("certbot.util.run_script") def test_get_version(self, mock_script): mock_script.return_value = ( "Server Version: Apache/2.4.2 (Debian)", "") @@ -717,21 +717,21 @@ class MultipleVhostsTest(util.ApacheTest): mock_script.side_effect = errors.SubprocessError("Can't find program") self.assertRaises(errors.PluginError, self.config.get_version) - @mock.patch("certbot_apache.configurator.le_util.run_script") + @mock.patch("certbot_apache.configurator.util.run_script") def test_restart(self, _): self.config.restart() - @mock.patch("certbot_apache.configurator.le_util.run_script") + @mock.patch("certbot_apache.configurator.util.run_script") def test_restart_bad_process(self, mock_run_script): mock_run_script.side_effect = [None, errors.SubprocessError] self.assertRaises(errors.MisconfigurationError, self.config.restart) - @mock.patch("certbot.le_util.run_script") + @mock.patch("certbot.util.run_script") def test_config_test(self, _): self.config.config_test() - @mock.patch("certbot.le_util.run_script") + @mock.patch("certbot.util.run_script") def test_config_test_bad_process(self, mock_run_script): mock_run_script.side_effect = errors.SubprocessError @@ -771,7 +771,7 @@ class MultipleVhostsTest(util.ApacheTest): @mock.patch("certbot_apache.configurator.ApacheConfigurator._get_http_vhost") @mock.patch("certbot_apache.display_ops.select_vhost") - @mock.patch("certbot.le_util.exe_exists") + @mock.patch("certbot.util.exe_exists") def test_enhance_unknown_vhost(self, mock_exe, mock_sel_vhost, mock_get): self.config.parser.modules.add("rewrite_module") mock_exe.return_value = True @@ -792,8 +792,8 @@ class MultipleVhostsTest(util.ApacheTest): errors.PluginError, self.config.enhance, "certbot.demo", "unknown_enhancement") - @mock.patch("certbot.le_util.run_script") - @mock.patch("certbot.le_util.exe_exists") + @mock.patch("certbot.util.run_script") + @mock.patch("certbot.util.exe_exists") def test_ocsp_stapling(self, mock_exe, mock_run_script): self.config.parser.update_runtime_variables = mock.Mock() self.config.parser.modules.add("mod_ssl.c") @@ -821,7 +821,7 @@ class MultipleVhostsTest(util.ApacheTest): self.assertEqual(len(stapling_cache_aug_path), 1) - @mock.patch("certbot.le_util.exe_exists") + @mock.patch("certbot.util.exe_exists") def test_ocsp_stapling_twice(self, mock_exe): self.config.parser.update_runtime_variables = mock.Mock() self.config.parser.modules.add("mod_ssl.c") @@ -848,7 +848,7 @@ class MultipleVhostsTest(util.ApacheTest): self.assertEqual(len(stapling_cache_aug_path), 1) - @mock.patch("certbot.le_util.exe_exists") + @mock.patch("certbot.util.exe_exists") def test_ocsp_unsupported_apache_version(self, mock_exe): mock_exe.return_value = True self.config.parser.update_runtime_variables = mock.Mock() @@ -871,8 +871,8 @@ class MultipleVhostsTest(util.ApacheTest): http_vh = self.config._get_http_vhost(ssl_vh) self.assertTrue(http_vh.ssl == False) - @mock.patch("certbot.le_util.run_script") - @mock.patch("certbot.le_util.exe_exists") + @mock.patch("certbot.util.run_script") + @mock.patch("certbot.util.exe_exists") def test_http_header_hsts(self, mock_exe, _): self.config.parser.update_runtime_variables = mock.Mock() self.config.parser.modules.add("mod_ssl.c") @@ -909,8 +909,8 @@ class MultipleVhostsTest(util.ApacheTest): self.config.enhance, "encryption-example.demo", "ensure-http-header", "Strict-Transport-Security") - @mock.patch("certbot.le_util.run_script") - @mock.patch("certbot.le_util.exe_exists") + @mock.patch("certbot.util.run_script") + @mock.patch("certbot.util.exe_exists") def test_http_header_uir(self, mock_exe, _): self.config.parser.update_runtime_variables = mock.Mock() self.config.parser.modules.add("mod_ssl.c") @@ -947,8 +947,8 @@ class MultipleVhostsTest(util.ApacheTest): self.config.enhance, "encryption-example.demo", "ensure-http-header", "Upgrade-Insecure-Requests") - @mock.patch("certbot.le_util.run_script") - @mock.patch("certbot.le_util.exe_exists") + @mock.patch("certbot.util.run_script") + @mock.patch("certbot.util.exe_exists") def test_redirect_well_formed_http(self, mock_exe, _): self.config.parser.update_runtime_variables = mock.Mock() mock_exe.return_value = True @@ -991,8 +991,8 @@ class MultipleVhostsTest(util.ApacheTest): # pylint: disable=protected-access self.assertTrue(self.config._is_rewrite_engine_on(self.vh_truth[3])) - @mock.patch("certbot.le_util.run_script") - @mock.patch("certbot.le_util.exe_exists") + @mock.patch("certbot.util.run_script") + @mock.patch("certbot.util.exe_exists") def test_redirect_with_existing_rewrite(self, mock_exe, _): self.config.parser.update_runtime_variables = mock.Mock() mock_exe.return_value = True diff --git a/certbot-apache/certbot_apache/tests/constants_test.py b/certbot-apache/certbot_apache/tests/constants_test.py index d970c96be..c040030df 100644 --- a/certbot-apache/certbot_apache/tests/constants_test.py +++ b/certbot-apache/certbot_apache/tests/constants_test.py @@ -8,19 +8,19 @@ from certbot_apache import constants class ConstantsTest(unittest.TestCase): - @mock.patch("certbot.le_util.get_os_info") + @mock.patch("certbot.util.get_os_info") def test_get_debian_value(self, os_info): os_info.return_value = ('Debian', '', '') self.assertEqual(constants.os_constant("vhost_root"), "/etc/apache2/sites-available") - @mock.patch("certbot.le_util.get_os_info") + @mock.patch("certbot.util.get_os_info") def test_get_centos_value(self, os_info): os_info.return_value = ('CentOS Linux', '', '') self.assertEqual(constants.os_constant("vhost_root"), "/etc/httpd/conf.d") - @mock.patch("certbot.le_util.get_os_info") + @mock.patch("certbot.util.get_os_info") def test_get_default_value(self, os_info): os_info.return_value = ('Nonexistent Linux', '', '') self.assertEqual(constants.os_constant("vhost_root"), diff --git a/certbot-apache/certbot_apache/tests/tls_sni_01_test.py b/certbot-apache/certbot_apache/tests/tls_sni_01_test.py index aa6a2a09c..5e369e3db 100644 --- a/certbot-apache/certbot_apache/tests/tls_sni_01_test.py +++ b/certbot-apache/certbot_apache/tests/tls_sni_01_test.py @@ -38,8 +38,8 @@ class TlsSniPerformTest(util.ApacheTest): resp = self.sni.perform() self.assertEqual(len(resp), 0) - @mock.patch("certbot.le_util.exe_exists") - @mock.patch("certbot.le_util.run_script") + @mock.patch("certbot.util.exe_exists") + @mock.patch("certbot.util.run_script") def test_perform1(self, _, mock_exists): mock_register = mock.Mock() self.sni.configurator.reverter.register_undo_command = mock_register diff --git a/certbot-apache/certbot_apache/tests/util.py b/certbot-apache/certbot_apache/tests/util.py index 8935ee908..050876687 100644 --- a/certbot-apache/certbot_apache/tests/util.py +++ b/certbot-apache/certbot_apache/tests/util.py @@ -95,8 +95,8 @@ def get_apache_configurator( in_progress_dir=os.path.join(backups, "IN_PROGRESS"), work_dir=work_dir) - with mock.patch("certbot_apache.configurator.le_util.run_script"): - with mock.patch("certbot_apache.configurator.le_util." + with mock.patch("certbot_apache.configurator.util.run_script"): + with mock.patch("certbot_apache.configurator.util." "exe_exists") as mock_exe_exists: mock_exe_exists.return_value = True with mock.patch("certbot_apache.parser.ApacheParser." From 2d121585c92a1f37eb51573984d5676228882df7 Mon Sep 17 00:00:00 2001 From: Blake Griffith Date: Thu, 26 May 2016 15:51:56 -0500 Subject: [PATCH 230/396] Catch more le_util in certbot-nginx --- certbot-nginx/certbot_nginx/tests/configurator_test.py | 8 ++++---- certbot-nginx/certbot_nginx/tests/util.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/certbot-nginx/certbot_nginx/tests/configurator_test.py b/certbot-nginx/certbot_nginx/tests/configurator_test.py index b36802939..30f287249 100644 --- a/certbot-nginx/certbot_nginx/tests/configurator_test.py +++ b/certbot-nginx/certbot_nginx/tests/configurator_test.py @@ -30,7 +30,7 @@ class NginxConfiguratorTest(util.NginxTest): shutil.rmtree(self.config_dir) shutil.rmtree(self.work_dir) - @mock.patch("certbot_nginx.configurator.le_util.exe_exists") + @mock.patch("certbot_nginx.configurator.util.exe_exists") def test_prepare_no_install(self, mock_exe_exists): mock_exe_exists.return_value = False self.assertRaises( @@ -40,7 +40,7 @@ class NginxConfiguratorTest(util.NginxTest): self.assertEquals((1, 6, 2), self.config.version) self.assertEquals(5, len(self.config.parser.parsed)) - @mock.patch("certbot_nginx.configurator.le_util.exe_exists") + @mock.patch("certbot_nginx.configurator.util.exe_exists") @mock.patch("certbot_nginx.configurator.subprocess.Popen") def test_prepare_initializes_version(self, mock_popen, mock_exe_exists): mock_popen().communicate.return_value = ( @@ -362,11 +362,11 @@ class NginxConfiguratorTest(util.NginxTest): mock_popen.side_effect = OSError("Can't find program") self.assertRaises(errors.MisconfigurationError, self.config.restart) - @mock.patch("certbot.le_util.run_script") + @mock.patch("certbot.util.run_script") def test_config_test(self, _): self.config.config_test() - @mock.patch("certbot.le_util.run_script") + @mock.patch("certbot.util.run_script") def test_config_test_bad_process(self, mock_run_script): mock_run_script.side_effect = errors.SubprocessError self.assertRaises(errors.MisconfigurationError, self.config.config_test) diff --git a/certbot-nginx/certbot_nginx/tests/util.py b/certbot-nginx/certbot_nginx/tests/util.py index 3c4731700..ddacd041b 100644 --- a/certbot-nginx/certbot_nginx/tests/util.py +++ b/certbot-nginx/certbot_nginx/tests/util.py @@ -51,7 +51,7 @@ def get_nginx_configurator( with mock.patch("certbot_nginx.configurator.NginxConfigurator." "config_test"): - with mock.patch("certbot_nginx.configurator.le_util." + with mock.patch("certbot_nginx.configurator.util." "exe_exists") as mock_exe_exists: mock_exe_exists.return_value = True config = configurator.NginxConfigurator( From b05258243a2b367632c207f3d2a14cc924258225 Mon Sep 17 00:00:00 2001 From: Blake Griffith Date: Thu, 26 May 2016 15:57:50 -0500 Subject: [PATCH 231/396] More le_util in docs and compatibility tests --- .../configurators/apache/common.py | 4 ++-- docs/api/le_util.rst | 5 ----- docs/api/util.rst | 5 +++++ 3 files changed, 7 insertions(+), 7 deletions(-) delete mode 100644 docs/api/le_util.rst create mode 100644 docs/api/util.rst diff --git a/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/common.py b/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/common.py index f57e0512d..9148666fc 100644 --- a/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/common.py +++ b/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/common.py @@ -47,10 +47,10 @@ class Proxy(configurators_common.Proxy): "certbot_apache.parser.subprocess", mock_subprocess).start() mock.patch( - "certbot.le_util.subprocess", + "certbot.util.subprocess", mock_subprocess).start() mock.patch( - "certbot_apache.configurator.le_util.exe_exists", + "certbot_apache.configurator.util.exe_exists", _is_apache_command).start() patch = mock.patch( diff --git a/docs/api/le_util.rst b/docs/api/le_util.rst deleted file mode 100644 index c9e332745..000000000 --- a/docs/api/le_util.rst +++ /dev/null @@ -1,5 +0,0 @@ -:mod:`certbot.le_util` --------------------------- - -.. automodule:: certbot.le_util - :members: diff --git a/docs/api/util.rst b/docs/api/util.rst new file mode 100644 index 000000000..7d0e33501 --- /dev/null +++ b/docs/api/util.rst @@ -0,0 +1,5 @@ +:mod:`certbot.util` +-------------------------- + +.. automodule:: certbot.util + :members: From 46d8f6e18c1d053a5811f55a619a31a75d21fc89 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Fri, 27 May 2016 13:30:46 -0700 Subject: [PATCH 232/396] Release 0.7.0 --- acme/setup.py | 2 +- certbot-apache/setup.py | 2 +- certbot-auto | 99 ++++++++++-------- certbot-compatibility-test/setup.py | 2 +- certbot-nginx/setup.py | 2 +- certbot/__init__.py | 2 +- docs/cli-help.txt | 32 ++++-- letsencrypt-apache/setup.py | 2 +- letsencrypt-auto | 99 ++++++++++-------- letsencrypt-auto-source/certbot-auto.asc | 14 +-- letsencrypt-auto-source/letsencrypt-auto | 32 +++--- letsencrypt-auto-source/letsencrypt-auto.sig | Bin 256 -> 256 bytes .../pieces/letsencrypt-auto-requirements.txt | 30 +++--- letsencrypt-nginx/setup.py | 2 +- letsencrypt/setup.py | 2 +- 15 files changed, 176 insertions(+), 146 deletions(-) diff --git a/acme/setup.py b/acme/setup.py index d38864dc1..3b01a6b73 100644 --- a/acme/setup.py +++ b/acme/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.7.0.dev0' +version = '0.7.0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot-apache/setup.py b/certbot-apache/setup.py index 56c6a451d..8974df882 100644 --- a/certbot-apache/setup.py +++ b/certbot-apache/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.7.0.dev0' +version = '0.7.0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot-auto b/certbot-auto index 8c6e6c486..5fbef43b1 100755 --- a/certbot-auto +++ b/certbot-auto @@ -19,7 +19,7 @@ XDG_DATA_HOME=${XDG_DATA_HOME:-~/.local/share} VENV_NAME="letsencrypt" VENV_PATH=${VENV_PATH:-"$XDG_DATA_HOME/$VENV_NAME"} VENV_BIN="$VENV_PATH/bin" -LE_AUTO_VERSION="0.6.0" +LE_AUTO_VERSION="0.7.0" BASENAME=$(basename $0) USAGE="Usage: $BASENAME [OPTIONS] A self-updating wrapper script for the Certbot ACME client. When run, updates @@ -38,17 +38,6 @@ Help for certbot itself cannot be provided until it is installed. All arguments are accepted and forwarded to the Certbot client when run." -while getopts ":hnv" arg; do - case $arg in - h) - HELP=1;; - n) - ASSUME_YES=1;; - v) - VERBOSE=1;; - esac -done - for arg in "$@" ; do case "$arg" in --debug) @@ -65,9 +54,26 @@ for arg in "$@" ; do ASSUME_YES=1;; --verbose) VERBOSE=1;; + -[!-]*) + while getopts ":hnv" short_arg $arg; do + case "$short_arg" in + h) + HELP=1;; + n) + ASSUME_YES=1;; + v) + VERBOSE=1;; + esac + done;; esac done +if [ $BASENAME = "letsencrypt-auto" ]; then + # letsencrypt-auto does not respect --help or --yes for backwards compatibility + ASSUME_YES=1 + HELP=0 +fi + # certbot-auto needs root access to bootstrap OS dependencies, and # certbot itself needs root access for almost all modes of operation # The "normal" case is that sudo is used for the steps that need root, but @@ -107,12 +113,6 @@ else SUDO= fi -if [ $BASENAME = "letsencrypt-auto" ]; then - # letsencrypt-auto does not respect --help or --yes for backwards compatibility - ASSUME_YES=1 - HELP=0 -fi - ExperimentalBootstrap() { # Arguments: Platform name, bootstrap function name if [ "$DEBUG" = 1 ]; then @@ -425,7 +425,8 @@ BootstrapMac() { $pkgcmd augeas $pkgcmd dialog - if [ "$(which python)" = "/System/Library/Frameworks/Python.framework/Versions/2.7/bin/python" ]; then + if [ "$(which python)" = "/System/Library/Frameworks/Python.framework/Versions/2.7/bin/python" \ + -o "$(which python)" = "/usr/bin/python" ]; then # We want to avoid using the system Python because it requires root to use pip. # python.org, MacPorts or HomeBrew Python installations should all be OK. echo "Installing python..." @@ -435,7 +436,8 @@ BootstrapMac() { # Workaround for _dlopen not finding augeas on OS X if [ "$pkgman" = "port" ] && ! [ -e "/usr/local/lib/libaugeas.dylib" ] && [ -e "/opt/local/lib/libaugeas.dylib" ]; then echo "Applying augeas workaround" - $SUDO ln -s /opt/local/lib/libaugeas.dylib /usr/local/lib + $SUDO mkdir -p /usr/local/lib/ + $SUDO ln -s /opt/local/lib/libaugeas.dylib /usr/local/lib/ fi if ! hash pip 2>/dev/null; then @@ -451,6 +453,11 @@ BootstrapMac() { fi } +BootstrapSmartOS() { + pkgin update + pkgin -y install 'gcc49' 'py27-augeas' 'py27-virtualenv' +} + # Install required OS packages: Bootstrap() { @@ -483,8 +490,10 @@ Bootstrap() { ExperimentalBootstrap "FreeBSD" BootstrapFreeBsd elif uname | grep -iq Darwin ; then ExperimentalBootstrap "Mac OS X" BootstrapMac - elif grep -iq "Amazon Linux" /etc/issue ; then + elif [ -f /etc/issue ] && grep -iq "Amazon Linux" /etc/issue ; then ExperimentalBootstrap "Amazon Linux" BootstrapRpmCommon + elif [ -f /etc/product ] && grep -q "Joyent Instance" /etc/product ; then + ExperimentalBootstrap "Joyent SmartOS Zone" BootstrapSmartOS else echo "Sorry, I don't know how to bootstrap Certbot on your operating system!" echo @@ -523,6 +532,7 @@ if [ "$1" = "--le-auto-phase2" ]; then echo "Installing Python packages..." TEMP_DIR=$(TempDir) + trap 'rm -rf "$TEMP_DIR"' EXIT # There is no $ interpolation due to quotes on starting heredoc delimiter. # ------------------------------------------------------------------------- cat << "UNLIKELY_EOF" > "$TEMP_DIR/letsencrypt-auto-requirements.txt" @@ -706,21 +716,21 @@ mock==1.0.1 \ # THE LINES BELOW ARE EDITED BY THE RELEASE SCRIPT; ADD ALL DEPENDENCIES ABOVE. -acme==0.6.0 \ - --hash=sha256:cbe4e7a340a19725a8740ed86e30abdbe18fc22c4c6022b7a8e56642d502bcc3 \ - --hash=sha256:ec4e6009dfbd629b58473eb06bbebfd9fb2a79fc8831c149e9205bc38a98ecc6 -certbot==0.6.0 \ - --hash=sha256:a893632d228864b0a751db9f3fdd93439ed34b988ea21b64fb0f0fa2ceded6a2 \ - --hash=sha256:80b0b7dc5afeec2816ef638a61e7c628d73cd72666eebf4984be426d1c2b492d -certbot-apache==0.6.0 \ - --hash=sha256:0ab077f0913b81ed5c1b141c3a7c4c0228ef3738d8d61a93db794d9a80718d43 \ - --hash=sha256:1cfbe751209079a803758f472200816fac559f2a36fdd582d25e3ba5601423a1 -letsencrypt==0.6.0 \ - --hash=sha256:93196c7dcd57272a753e525d145c5a9987c8968c22ec954bcf83dcc9d2499a76 \ - --hash=sha256:a16d6c395f1bf5fd61a28ef83dc78f42dbecbad9d00be6236f2ad8915645c154 -letsencrypt-apache==0.6.0 \ - --hash=sha256:02fadc52a0796e53978c508beec9c53e1fc047660240832b9bde5d53ab3a1379 \ - --hash=sha256:1c5522d94d7750bdb9bfa6201d2c263e914f662c9d0079e673167233cf4364f1 +acme==0.7.0 \ + --hash=sha256:6e61dba343806ad4cb27af84628152abc9e83a0fa24be6065587d2b46f340d7a \ + --hash=sha256:9f75a1947978402026b741bdee8a18fc5a1cfd539b78e523b7e5f279bf18eeb9 +certbot==0.7.0 \ + --hash=sha256:55604e43d231ac226edefed8dc110d792052095c3d75ad0e4a228ae0989fe5fd \ + --hash=sha256:ad5083d75e16d1ab806802d3a32f34973b6d7adaf083aee87e07a6c1359efe88 +certbot-apache==0.7.0 \ + --hash=sha256:5ab5ed9b2af6c7db9495ce1491122798e9d0764e3df8f0843d11d89690bf7f88 \ + --hash=sha256:1ddbfaf01bcb0b05c0dcc8b2ebd37637f080cf798151e8140c20c9f5fe7bae75 +letsencrypt==0.7.0 \ + --hash=sha256:105a5fb107e45bcd0722eb89696986dcf5f08a86a321d6aef25a0c7c63375ade \ + --hash=sha256:c36e532c486a7e92155ee09da54b436a3c420813ec1c590b98f635d924720de9 +letsencrypt-apache==0.7.0 \ + --hash=sha256:10445980a6afc810325ea22a56e269229999120848f6c0b323b00275696b5c80 \ + --hash=sha256:3f4656088a18e4efea7cd7eb4965e14e8d901f3b64f4691e79cafd0bb91890f0 UNLIKELY_EOF # ------------------------------------------------------------------------- @@ -880,7 +890,6 @@ UNLIKELY_EOF PIP_OUT=`"$VENV_BIN/pip" install --no-cache-dir --require-hashes -r "$TEMP_DIR/letsencrypt-auto-requirements.txt" 2>&1` PIP_STATUS=$? set -e - rm -rf "$TEMP_DIR" if [ "$PIP_STATUS" != 0 ]; then # Report error. (Otherwise, be quiet.) echo "Had a problem while installing Python packages:" @@ -890,14 +899,16 @@ UNLIKELY_EOF fi echo "Installation succeeded." fi - echo "Requesting root privileges to run certbot..." + if [ -n "$SUDO" ]; then + # SUDO is su wrapper or sudo + echo "Requesting root privileges to run certbot..." + echo " $VENV_BIN/letsencrypt" "$@" + fi if [ -z "$SUDO_ENV" ] ; then # SUDO is su wrapper / noop - echo " " $SUDO "$VENV_BIN/letsencrypt" "$@" $SUDO "$VENV_BIN/letsencrypt" "$@" else # sudo - echo " " $SUDO "$SUDO_ENV" "$VENV_BIN/letsencrypt" "$@" $SUDO "$SUDO_ENV" "$VENV_BIN/letsencrypt" "$@" fi @@ -923,8 +934,8 @@ else fi if [ "$NO_SELF_UPGRADE" != 1 ]; then - echo "Checking for new version..." TEMP_DIR=$(TempDir) + trap 'rm -rf "$TEMP_DIR"' EXIT # --------------------------------------------------------------------------- cat << "UNLIKELY_EOF" > "$TEMP_DIR/fetch.py" """Do downloading and JSON parsing without additional dependencies. :: @@ -997,7 +1008,7 @@ def latest_stable_version(get): """Return the latest stable release of letsencrypt.""" metadata = loads(get( environ.get('LE_AUTO_JSON_URL', - 'https://pypi.python.org/pypi/letsencrypt/json'))) + 'https://pypi.python.org/pypi/certbot/json'))) # metadata['info']['version'] actually returns the latest of any kind of # release release, contrary to https://wiki.python.org/moin/PyPIJSON. # The regex is a sufficient regex for picking out prereleases for most @@ -1016,7 +1027,7 @@ def verified_new_le_auto(get, tag, temp_dir): """ le_auto_dir = environ.get( 'LE_AUTO_DIR_TEMPLATE', - 'https://raw.githubusercontent.com/letsencrypt/letsencrypt/%s/' + 'https://raw.githubusercontent.com/certbot/certbot/%s/' 'letsencrypt-auto-source/') % tag write(get(le_auto_dir + 'letsencrypt-auto'), temp_dir, 'letsencrypt-auto') write(get(le_auto_dir + 'letsencrypt-auto.sig'), temp_dir, 'letsencrypt-auto.sig') @@ -1079,8 +1090,6 @@ UNLIKELY_EOF # filesystems is non-atomic, doing `rm dest, cp src dest, rm src`, but the # cp is unlikely to fail (esp. under sudo) if the rm doesn't. $SUDO mv -f "$TEMP_DIR/letsencrypt-auto.permission-clone" "$0" - # TODO: Clean up temp dir safely, even if it has quotes in its path. - rm -rf "$TEMP_DIR" fi # A newer version is available. fi # Self-upgrading is allowed. diff --git a/certbot-compatibility-test/setup.py b/certbot-compatibility-test/setup.py index 8f9452c5a..b1196eb23 100644 --- a/certbot-compatibility-test/setup.py +++ b/certbot-compatibility-test/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.7.0.dev0' +version = '0.7.0' install_requires = [ 'certbot=={0}'.format(version), diff --git a/certbot-nginx/setup.py b/certbot-nginx/setup.py index a74b93093..bfdfd5f66 100644 --- a/certbot-nginx/setup.py +++ b/certbot-nginx/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.7.0.dev0' +version = '0.7.0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot/__init__.py b/certbot/__init__.py index 85f370e7a..06a1930db 100644 --- a/certbot/__init__.py +++ b/certbot/__init__.py @@ -1,4 +1,4 @@ """Certbot client.""" # version number like 1.2.3a0, must have at least 2 parts, like 1.2 -__version__ = '0.7.0.dev0' +__version__ = '0.7.0' diff --git a/docs/cli-help.txt b/docs/cli-help.txt index cb4bace58..4026f1cc8 100644 --- a/docs/cli-help.txt +++ b/docs/cli-help.txt @@ -28,6 +28,7 @@ optional arguments: require additional command line flags; the client will try to explain which ones are required if it finds one missing (default: False) + --dialog Run using dialog (default: False) --dry-run Perform a test run of the client, obtaining test (invalid) certs but not saving them to disk. This can currently only be used with the 'certonly' and 'renew' @@ -130,6 +131,10 @@ security: Security parameters & server settings --rsa-key-size N Size of the RSA key. (default: 2048) + --must-staple Adds the OCSP Must Staple extension to the + certificate. Autoconfigures OCSP Stapling for + supported setups (Apache version >= 2.3.3 ). (default: + False) --redirect Automatically redirect all HTTP traffic to HTTPS for the newly authenticated vhost. (default: None) --no-redirect Do not automatically redirect all HTTP traffic to @@ -148,6 +153,11 @@ security: --no-uir Do not automatically set the "Content-Security-Policy: upgrade-insecure-requests" header to every HTTP response. (default: None) + --staple-ocsp Enables OCSP Stapling. A valid OCSP response is + stapled to the certificate that the server offers + during TLS. (default: None) + --no-staple-ocsp Do not automatically enable OCSP Stapling. (default: + None) --strict-permissions Require that all configuration files are owned by the current user; only needed if your config is somewhere unsafe like /tmp/ (default: False) @@ -173,7 +183,9 @@ renew: Command to be run in a shell after attempting to obtain/renew certificates. Can be used to deploy renewed certificates, or to restart any servers that - were stopped by --pre-hook. (default: None) + were stopped by --pre-hook. This is only run if an + attempt was made to obtain/renew a certificate. + (default: None) --renew-hook RENEW_HOOK Command to be run in a shell once for each successfully renewed certificate.For this command, the @@ -263,15 +275,6 @@ plugins: --webroot Obtain certs by placing files in a webroot directory. (default: False) -nginx: - Nginx Web Server - currently doesn't work - - --nginx-server-root NGINX_SERVER_ROOT - Nginx server root directory. (default: /etc/nginx) - --nginx-ctl NGINX_CTL - Path to the 'nginx' binary, used for 'configtest' and - retrieving nginx version number. (default: nginx) - standalone: Automatically use a temporary webserver @@ -288,6 +291,15 @@ manual: Automatically allows public IP logging. (default: False) +nginx: + Nginx Web Server - currently doesn't work + + --nginx-server-root NGINX_SERVER_ROOT + Nginx server root directory. (default: /etc/nginx) + --nginx-ctl NGINX_CTL + Path to the 'nginx' binary, used for 'configtest' and + retrieving nginx version number. (default: nginx) + webroot: Place files in webroot directory diff --git a/letsencrypt-apache/setup.py b/letsencrypt-apache/setup.py index b94746150..29b3df09f 100644 --- a/letsencrypt-apache/setup.py +++ b/letsencrypt-apache/setup.py @@ -16,7 +16,7 @@ here = os.path.abspath(os.path.dirname(__file__)) readme = read_file(os.path.join(here, 'README.rst')) -version = '0.7.0.dev0' +version = '0.7.0' # This package is a simple shim around certbot-apache diff --git a/letsencrypt-auto b/letsencrypt-auto index 8c6e6c486..5fbef43b1 100755 --- a/letsencrypt-auto +++ b/letsencrypt-auto @@ -19,7 +19,7 @@ XDG_DATA_HOME=${XDG_DATA_HOME:-~/.local/share} VENV_NAME="letsencrypt" VENV_PATH=${VENV_PATH:-"$XDG_DATA_HOME/$VENV_NAME"} VENV_BIN="$VENV_PATH/bin" -LE_AUTO_VERSION="0.6.0" +LE_AUTO_VERSION="0.7.0" BASENAME=$(basename $0) USAGE="Usage: $BASENAME [OPTIONS] A self-updating wrapper script for the Certbot ACME client. When run, updates @@ -38,17 +38,6 @@ Help for certbot itself cannot be provided until it is installed. All arguments are accepted and forwarded to the Certbot client when run." -while getopts ":hnv" arg; do - case $arg in - h) - HELP=1;; - n) - ASSUME_YES=1;; - v) - VERBOSE=1;; - esac -done - for arg in "$@" ; do case "$arg" in --debug) @@ -65,9 +54,26 @@ for arg in "$@" ; do ASSUME_YES=1;; --verbose) VERBOSE=1;; + -[!-]*) + while getopts ":hnv" short_arg $arg; do + case "$short_arg" in + h) + HELP=1;; + n) + ASSUME_YES=1;; + v) + VERBOSE=1;; + esac + done;; esac done +if [ $BASENAME = "letsencrypt-auto" ]; then + # letsencrypt-auto does not respect --help or --yes for backwards compatibility + ASSUME_YES=1 + HELP=0 +fi + # certbot-auto needs root access to bootstrap OS dependencies, and # certbot itself needs root access for almost all modes of operation # The "normal" case is that sudo is used for the steps that need root, but @@ -107,12 +113,6 @@ else SUDO= fi -if [ $BASENAME = "letsencrypt-auto" ]; then - # letsencrypt-auto does not respect --help or --yes for backwards compatibility - ASSUME_YES=1 - HELP=0 -fi - ExperimentalBootstrap() { # Arguments: Platform name, bootstrap function name if [ "$DEBUG" = 1 ]; then @@ -425,7 +425,8 @@ BootstrapMac() { $pkgcmd augeas $pkgcmd dialog - if [ "$(which python)" = "/System/Library/Frameworks/Python.framework/Versions/2.7/bin/python" ]; then + if [ "$(which python)" = "/System/Library/Frameworks/Python.framework/Versions/2.7/bin/python" \ + -o "$(which python)" = "/usr/bin/python" ]; then # We want to avoid using the system Python because it requires root to use pip. # python.org, MacPorts or HomeBrew Python installations should all be OK. echo "Installing python..." @@ -435,7 +436,8 @@ BootstrapMac() { # Workaround for _dlopen not finding augeas on OS X if [ "$pkgman" = "port" ] && ! [ -e "/usr/local/lib/libaugeas.dylib" ] && [ -e "/opt/local/lib/libaugeas.dylib" ]; then echo "Applying augeas workaround" - $SUDO ln -s /opt/local/lib/libaugeas.dylib /usr/local/lib + $SUDO mkdir -p /usr/local/lib/ + $SUDO ln -s /opt/local/lib/libaugeas.dylib /usr/local/lib/ fi if ! hash pip 2>/dev/null; then @@ -451,6 +453,11 @@ BootstrapMac() { fi } +BootstrapSmartOS() { + pkgin update + pkgin -y install 'gcc49' 'py27-augeas' 'py27-virtualenv' +} + # Install required OS packages: Bootstrap() { @@ -483,8 +490,10 @@ Bootstrap() { ExperimentalBootstrap "FreeBSD" BootstrapFreeBsd elif uname | grep -iq Darwin ; then ExperimentalBootstrap "Mac OS X" BootstrapMac - elif grep -iq "Amazon Linux" /etc/issue ; then + elif [ -f /etc/issue ] && grep -iq "Amazon Linux" /etc/issue ; then ExperimentalBootstrap "Amazon Linux" BootstrapRpmCommon + elif [ -f /etc/product ] && grep -q "Joyent Instance" /etc/product ; then + ExperimentalBootstrap "Joyent SmartOS Zone" BootstrapSmartOS else echo "Sorry, I don't know how to bootstrap Certbot on your operating system!" echo @@ -523,6 +532,7 @@ if [ "$1" = "--le-auto-phase2" ]; then echo "Installing Python packages..." TEMP_DIR=$(TempDir) + trap 'rm -rf "$TEMP_DIR"' EXIT # There is no $ interpolation due to quotes on starting heredoc delimiter. # ------------------------------------------------------------------------- cat << "UNLIKELY_EOF" > "$TEMP_DIR/letsencrypt-auto-requirements.txt" @@ -706,21 +716,21 @@ mock==1.0.1 \ # THE LINES BELOW ARE EDITED BY THE RELEASE SCRIPT; ADD ALL DEPENDENCIES ABOVE. -acme==0.6.0 \ - --hash=sha256:cbe4e7a340a19725a8740ed86e30abdbe18fc22c4c6022b7a8e56642d502bcc3 \ - --hash=sha256:ec4e6009dfbd629b58473eb06bbebfd9fb2a79fc8831c149e9205bc38a98ecc6 -certbot==0.6.0 \ - --hash=sha256:a893632d228864b0a751db9f3fdd93439ed34b988ea21b64fb0f0fa2ceded6a2 \ - --hash=sha256:80b0b7dc5afeec2816ef638a61e7c628d73cd72666eebf4984be426d1c2b492d -certbot-apache==0.6.0 \ - --hash=sha256:0ab077f0913b81ed5c1b141c3a7c4c0228ef3738d8d61a93db794d9a80718d43 \ - --hash=sha256:1cfbe751209079a803758f472200816fac559f2a36fdd582d25e3ba5601423a1 -letsencrypt==0.6.0 \ - --hash=sha256:93196c7dcd57272a753e525d145c5a9987c8968c22ec954bcf83dcc9d2499a76 \ - --hash=sha256:a16d6c395f1bf5fd61a28ef83dc78f42dbecbad9d00be6236f2ad8915645c154 -letsencrypt-apache==0.6.0 \ - --hash=sha256:02fadc52a0796e53978c508beec9c53e1fc047660240832b9bde5d53ab3a1379 \ - --hash=sha256:1c5522d94d7750bdb9bfa6201d2c263e914f662c9d0079e673167233cf4364f1 +acme==0.7.0 \ + --hash=sha256:6e61dba343806ad4cb27af84628152abc9e83a0fa24be6065587d2b46f340d7a \ + --hash=sha256:9f75a1947978402026b741bdee8a18fc5a1cfd539b78e523b7e5f279bf18eeb9 +certbot==0.7.0 \ + --hash=sha256:55604e43d231ac226edefed8dc110d792052095c3d75ad0e4a228ae0989fe5fd \ + --hash=sha256:ad5083d75e16d1ab806802d3a32f34973b6d7adaf083aee87e07a6c1359efe88 +certbot-apache==0.7.0 \ + --hash=sha256:5ab5ed9b2af6c7db9495ce1491122798e9d0764e3df8f0843d11d89690bf7f88 \ + --hash=sha256:1ddbfaf01bcb0b05c0dcc8b2ebd37637f080cf798151e8140c20c9f5fe7bae75 +letsencrypt==0.7.0 \ + --hash=sha256:105a5fb107e45bcd0722eb89696986dcf5f08a86a321d6aef25a0c7c63375ade \ + --hash=sha256:c36e532c486a7e92155ee09da54b436a3c420813ec1c590b98f635d924720de9 +letsencrypt-apache==0.7.0 \ + --hash=sha256:10445980a6afc810325ea22a56e269229999120848f6c0b323b00275696b5c80 \ + --hash=sha256:3f4656088a18e4efea7cd7eb4965e14e8d901f3b64f4691e79cafd0bb91890f0 UNLIKELY_EOF # ------------------------------------------------------------------------- @@ -880,7 +890,6 @@ UNLIKELY_EOF PIP_OUT=`"$VENV_BIN/pip" install --no-cache-dir --require-hashes -r "$TEMP_DIR/letsencrypt-auto-requirements.txt" 2>&1` PIP_STATUS=$? set -e - rm -rf "$TEMP_DIR" if [ "$PIP_STATUS" != 0 ]; then # Report error. (Otherwise, be quiet.) echo "Had a problem while installing Python packages:" @@ -890,14 +899,16 @@ UNLIKELY_EOF fi echo "Installation succeeded." fi - echo "Requesting root privileges to run certbot..." + if [ -n "$SUDO" ]; then + # SUDO is su wrapper or sudo + echo "Requesting root privileges to run certbot..." + echo " $VENV_BIN/letsencrypt" "$@" + fi if [ -z "$SUDO_ENV" ] ; then # SUDO is su wrapper / noop - echo " " $SUDO "$VENV_BIN/letsencrypt" "$@" $SUDO "$VENV_BIN/letsencrypt" "$@" else # sudo - echo " " $SUDO "$SUDO_ENV" "$VENV_BIN/letsencrypt" "$@" $SUDO "$SUDO_ENV" "$VENV_BIN/letsencrypt" "$@" fi @@ -923,8 +934,8 @@ else fi if [ "$NO_SELF_UPGRADE" != 1 ]; then - echo "Checking for new version..." TEMP_DIR=$(TempDir) + trap 'rm -rf "$TEMP_DIR"' EXIT # --------------------------------------------------------------------------- cat << "UNLIKELY_EOF" > "$TEMP_DIR/fetch.py" """Do downloading and JSON parsing without additional dependencies. :: @@ -997,7 +1008,7 @@ def latest_stable_version(get): """Return the latest stable release of letsencrypt.""" metadata = loads(get( environ.get('LE_AUTO_JSON_URL', - 'https://pypi.python.org/pypi/letsencrypt/json'))) + 'https://pypi.python.org/pypi/certbot/json'))) # metadata['info']['version'] actually returns the latest of any kind of # release release, contrary to https://wiki.python.org/moin/PyPIJSON. # The regex is a sufficient regex for picking out prereleases for most @@ -1016,7 +1027,7 @@ def verified_new_le_auto(get, tag, temp_dir): """ le_auto_dir = environ.get( 'LE_AUTO_DIR_TEMPLATE', - 'https://raw.githubusercontent.com/letsencrypt/letsencrypt/%s/' + 'https://raw.githubusercontent.com/certbot/certbot/%s/' 'letsencrypt-auto-source/') % tag write(get(le_auto_dir + 'letsencrypt-auto'), temp_dir, 'letsencrypt-auto') write(get(le_auto_dir + 'letsencrypt-auto.sig'), temp_dir, 'letsencrypt-auto.sig') @@ -1079,8 +1090,6 @@ UNLIKELY_EOF # filesystems is non-atomic, doing `rm dest, cp src dest, rm src`, but the # cp is unlikely to fail (esp. under sudo) if the rm doesn't. $SUDO mv -f "$TEMP_DIR/letsencrypt-auto.permission-clone" "$0" - # TODO: Clean up temp dir safely, even if it has quotes in its path. - rm -rf "$TEMP_DIR" fi # A newer version is available. fi # Self-upgrading is allowed. diff --git a/letsencrypt-auto-source/certbot-auto.asc b/letsencrypt-auto-source/certbot-auto.asc index 8b4f34c70..454cbe598 100644 --- a/letsencrypt-auto-source/certbot-auto.asc +++ b/letsencrypt-auto-source/certbot-auto.asc @@ -1,11 +1,11 @@ -----BEGIN PGP SIGNATURE----- Version: GnuPG v1 -iQEcBAABAgAGBQJXM9ZDAAoJEE0XyZXNl3XyzGkH/2KeR0jYxXKlvwfCkxU6hSC0 -eXcxZVQk59hCSvkNGE6Mj6rwQcyjSqmRp14MaJpq7NZADN6F+HWb6VB/Wq6moMQs -PJtthqwhF767Qg+Py9Hp6XmlKscjXB6AKCVxq5TBwEIOTtj0rhQRLF9/+GW6jFuf -kT6aUcDWNjOyWWUtp9vOVprDtegrltp0/2DNitlvPu263pKC+7I3GyLTq4fKP4EE -auZSAhFry9SNR3Usf2wD3kzhvLSrT3h9Yh5oA04oaX9H6e86EHwt6RJJRHpg8s6b -e0CBIIuaRJEmdiMUWlV/gAfH6M2PbG1wtJdxc0ThNEoWAjTsopr61BoHJ3cpCy4= -=+e7/ +iQEcBAABAgAGBQJXSK5DAAoJEE0XyZXNl3Xyyb4H/Ahy9/8ADDaN5V/O/6kl6gE5 +amQfm8T10EUD8APnNWYrYKBYruDBVvH0KiEcuAEs7q4xE5BaQatlobSnsHfv4AWW +TwInk2lRxYZ++MwwQf3DrqMK5QKfcoVnViZsRpZ8gHMLzsJllRm7R5eaTewO2ViM +KM+yDB3UsquLUvE4d3/hgBl2mXAUwsxLeFreZayvpoTcX2ARnzbtKqMaIBYDYWcx +DewWtDsPrhKFpb2DY06S6JLmEttysUgv+hbKlaVO0yZ8cCUehkzBIGYoeS4chOLq +fonNCzB8u3RtnLEFiPIy0N+A592jbLsqqUkxjammaJq3lH7nitduMLnpvGKt4yc= +=ex1J -----END PGP SIGNATURE----- diff --git a/letsencrypt-auto-source/letsencrypt-auto b/letsencrypt-auto-source/letsencrypt-auto index fee63c2f5..5fbef43b1 100755 --- a/letsencrypt-auto-source/letsencrypt-auto +++ b/letsencrypt-auto-source/letsencrypt-auto @@ -19,7 +19,7 @@ XDG_DATA_HOME=${XDG_DATA_HOME:-~/.local/share} VENV_NAME="letsencrypt" VENV_PATH=${VENV_PATH:-"$XDG_DATA_HOME/$VENV_NAME"} VENV_BIN="$VENV_PATH/bin" -LE_AUTO_VERSION="0.7.0.dev0" +LE_AUTO_VERSION="0.7.0" BASENAME=$(basename $0) USAGE="Usage: $BASENAME [OPTIONS] A self-updating wrapper script for the Certbot ACME client. When run, updates @@ -716,21 +716,21 @@ mock==1.0.1 \ # THE LINES BELOW ARE EDITED BY THE RELEASE SCRIPT; ADD ALL DEPENDENCIES ABOVE. -acme==0.6.0 \ - --hash=sha256:cbe4e7a340a19725a8740ed86e30abdbe18fc22c4c6022b7a8e56642d502bcc3 \ - --hash=sha256:ec4e6009dfbd629b58473eb06bbebfd9fb2a79fc8831c149e9205bc38a98ecc6 -certbot==0.6.0 \ - --hash=sha256:a893632d228864b0a751db9f3fdd93439ed34b988ea21b64fb0f0fa2ceded6a2 \ - --hash=sha256:80b0b7dc5afeec2816ef638a61e7c628d73cd72666eebf4984be426d1c2b492d -certbot-apache==0.6.0 \ - --hash=sha256:0ab077f0913b81ed5c1b141c3a7c4c0228ef3738d8d61a93db794d9a80718d43 \ - --hash=sha256:1cfbe751209079a803758f472200816fac559f2a36fdd582d25e3ba5601423a1 -letsencrypt==0.6.0 \ - --hash=sha256:93196c7dcd57272a753e525d145c5a9987c8968c22ec954bcf83dcc9d2499a76 \ - --hash=sha256:a16d6c395f1bf5fd61a28ef83dc78f42dbecbad9d00be6236f2ad8915645c154 -letsencrypt-apache==0.6.0 \ - --hash=sha256:02fadc52a0796e53978c508beec9c53e1fc047660240832b9bde5d53ab3a1379 \ - --hash=sha256:1c5522d94d7750bdb9bfa6201d2c263e914f662c9d0079e673167233cf4364f1 +acme==0.7.0 \ + --hash=sha256:6e61dba343806ad4cb27af84628152abc9e83a0fa24be6065587d2b46f340d7a \ + --hash=sha256:9f75a1947978402026b741bdee8a18fc5a1cfd539b78e523b7e5f279bf18eeb9 +certbot==0.7.0 \ + --hash=sha256:55604e43d231ac226edefed8dc110d792052095c3d75ad0e4a228ae0989fe5fd \ + --hash=sha256:ad5083d75e16d1ab806802d3a32f34973b6d7adaf083aee87e07a6c1359efe88 +certbot-apache==0.7.0 \ + --hash=sha256:5ab5ed9b2af6c7db9495ce1491122798e9d0764e3df8f0843d11d89690bf7f88 \ + --hash=sha256:1ddbfaf01bcb0b05c0dcc8b2ebd37637f080cf798151e8140c20c9f5fe7bae75 +letsencrypt==0.7.0 \ + --hash=sha256:105a5fb107e45bcd0722eb89696986dcf5f08a86a321d6aef25a0c7c63375ade \ + --hash=sha256:c36e532c486a7e92155ee09da54b436a3c420813ec1c590b98f635d924720de9 +letsencrypt-apache==0.7.0 \ + --hash=sha256:10445980a6afc810325ea22a56e269229999120848f6c0b323b00275696b5c80 \ + --hash=sha256:3f4656088a18e4efea7cd7eb4965e14e8d901f3b64f4691e79cafd0bb91890f0 UNLIKELY_EOF # ------------------------------------------------------------------------- diff --git a/letsencrypt-auto-source/letsencrypt-auto.sig b/letsencrypt-auto-source/letsencrypt-auto.sig index cb360f89ef5443af2c6bc157704a98d3cd68fcef..e7e6546a25178083f007351bbba85fda56c87bb2 100644 GIT binary patch literal 256 zcmV+b0ssD(3r1$c09~FFRL*(rn=D5xr$W>4tf{yJ0Yd>ifFOs3`#iS_w(uP+p(fG& zTEmPXK+5%ZpiHyaPtmN4i1xvJn%fgm9b2nF4!4kuT8ov;Uq`~S8bQzaVDw}C5Xzw8cyZq9H=SydJxxr0AB GYGOMK)`FS< diff --git a/letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt b/letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt index f4ceae536..6405efd78 100644 --- a/letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt +++ b/letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt @@ -178,18 +178,18 @@ mock==1.0.1 \ # THE LINES BELOW ARE EDITED BY THE RELEASE SCRIPT; ADD ALL DEPENDENCIES ABOVE. -acme==0.6.0 \ - --hash=sha256:cbe4e7a340a19725a8740ed86e30abdbe18fc22c4c6022b7a8e56642d502bcc3 \ - --hash=sha256:ec4e6009dfbd629b58473eb06bbebfd9fb2a79fc8831c149e9205bc38a98ecc6 -certbot==0.6.0 \ - --hash=sha256:a893632d228864b0a751db9f3fdd93439ed34b988ea21b64fb0f0fa2ceded6a2 \ - --hash=sha256:80b0b7dc5afeec2816ef638a61e7c628d73cd72666eebf4984be426d1c2b492d -certbot-apache==0.6.0 \ - --hash=sha256:0ab077f0913b81ed5c1b141c3a7c4c0228ef3738d8d61a93db794d9a80718d43 \ - --hash=sha256:1cfbe751209079a803758f472200816fac559f2a36fdd582d25e3ba5601423a1 -letsencrypt==0.6.0 \ - --hash=sha256:93196c7dcd57272a753e525d145c5a9987c8968c22ec954bcf83dcc9d2499a76 \ - --hash=sha256:a16d6c395f1bf5fd61a28ef83dc78f42dbecbad9d00be6236f2ad8915645c154 -letsencrypt-apache==0.6.0 \ - --hash=sha256:02fadc52a0796e53978c508beec9c53e1fc047660240832b9bde5d53ab3a1379 \ - --hash=sha256:1c5522d94d7750bdb9bfa6201d2c263e914f662c9d0079e673167233cf4364f1 +acme==0.7.0 \ + --hash=sha256:6e61dba343806ad4cb27af84628152abc9e83a0fa24be6065587d2b46f340d7a \ + --hash=sha256:9f75a1947978402026b741bdee8a18fc5a1cfd539b78e523b7e5f279bf18eeb9 +certbot==0.7.0 \ + --hash=sha256:55604e43d231ac226edefed8dc110d792052095c3d75ad0e4a228ae0989fe5fd \ + --hash=sha256:ad5083d75e16d1ab806802d3a32f34973b6d7adaf083aee87e07a6c1359efe88 +certbot-apache==0.7.0 \ + --hash=sha256:5ab5ed9b2af6c7db9495ce1491122798e9d0764e3df8f0843d11d89690bf7f88 \ + --hash=sha256:1ddbfaf01bcb0b05c0dcc8b2ebd37637f080cf798151e8140c20c9f5fe7bae75 +letsencrypt==0.7.0 \ + --hash=sha256:105a5fb107e45bcd0722eb89696986dcf5f08a86a321d6aef25a0c7c63375ade \ + --hash=sha256:c36e532c486a7e92155ee09da54b436a3c420813ec1c590b98f635d924720de9 +letsencrypt-apache==0.7.0 \ + --hash=sha256:10445980a6afc810325ea22a56e269229999120848f6c0b323b00275696b5c80 \ + --hash=sha256:3f4656088a18e4efea7cd7eb4965e14e8d901f3b64f4691e79cafd0bb91890f0 diff --git a/letsencrypt-nginx/setup.py b/letsencrypt-nginx/setup.py index 95ffd6cd8..3acfa9bb8 100644 --- a/letsencrypt-nginx/setup.py +++ b/letsencrypt-nginx/setup.py @@ -16,7 +16,7 @@ here = os.path.abspath(os.path.dirname(__file__)) readme = read_file(os.path.join(here, 'README.rst')) -version = '0.7.0.dev0' +version = '0.7.0' # This package is a simple shim around certbot-nginx diff --git a/letsencrypt/setup.py b/letsencrypt/setup.py index 7c974ea9b..fd6544265 100644 --- a/letsencrypt/setup.py +++ b/letsencrypt/setup.py @@ -20,7 +20,7 @@ readme = read_file(os.path.join(here, 'README.rst')) install_requires = ['certbot'] -version = '0.7.0.dev0' +version = '0.7.0' setup( From 7153220b4106031fcf7cc4a958d0a3f44215d6f8 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Fri, 27 May 2016 13:30:54 -0700 Subject: [PATCH 233/396] Bump version to 0.8.0 --- acme/setup.py | 2 +- certbot-apache/setup.py | 2 +- certbot-compatibility-test/setup.py | 2 +- certbot-nginx/setup.py | 2 +- certbot/__init__.py | 2 +- letsencrypt-apache/setup.py | 2 +- letsencrypt-auto-source/letsencrypt-auto | 2 +- letsencrypt-nginx/setup.py | 2 +- letsencrypt/setup.py | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/acme/setup.py b/acme/setup.py index 3b01a6b73..c25cb5c00 100644 --- a/acme/setup.py +++ b/acme/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.7.0' +version = '0.8.0.dev0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot-apache/setup.py b/certbot-apache/setup.py index 8974df882..2a4716db7 100644 --- a/certbot-apache/setup.py +++ b/certbot-apache/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.7.0' +version = '0.8.0.dev0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot-compatibility-test/setup.py b/certbot-compatibility-test/setup.py index b1196eb23..8d2bd925d 100644 --- a/certbot-compatibility-test/setup.py +++ b/certbot-compatibility-test/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.7.0' +version = '0.8.0.dev0' install_requires = [ 'certbot=={0}'.format(version), diff --git a/certbot-nginx/setup.py b/certbot-nginx/setup.py index bfdfd5f66..bb8b3414e 100644 --- a/certbot-nginx/setup.py +++ b/certbot-nginx/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.7.0' +version = '0.8.0.dev0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot/__init__.py b/certbot/__init__.py index 06a1930db..dc0e2764d 100644 --- a/certbot/__init__.py +++ b/certbot/__init__.py @@ -1,4 +1,4 @@ """Certbot client.""" # version number like 1.2.3a0, must have at least 2 parts, like 1.2 -__version__ = '0.7.0' +__version__ = '0.8.0.dev0' diff --git a/letsencrypt-apache/setup.py b/letsencrypt-apache/setup.py index 29b3df09f..09703841c 100644 --- a/letsencrypt-apache/setup.py +++ b/letsencrypt-apache/setup.py @@ -16,7 +16,7 @@ here = os.path.abspath(os.path.dirname(__file__)) readme = read_file(os.path.join(here, 'README.rst')) -version = '0.7.0' +version = '0.8.0.dev0' # This package is a simple shim around certbot-apache diff --git a/letsencrypt-auto-source/letsencrypt-auto b/letsencrypt-auto-source/letsencrypt-auto index 5fbef43b1..2de0652a9 100755 --- a/letsencrypt-auto-source/letsencrypt-auto +++ b/letsencrypt-auto-source/letsencrypt-auto @@ -19,7 +19,7 @@ XDG_DATA_HOME=${XDG_DATA_HOME:-~/.local/share} VENV_NAME="letsencrypt" VENV_PATH=${VENV_PATH:-"$XDG_DATA_HOME/$VENV_NAME"} VENV_BIN="$VENV_PATH/bin" -LE_AUTO_VERSION="0.7.0" +LE_AUTO_VERSION="0.8.0.dev0" BASENAME=$(basename $0) USAGE="Usage: $BASENAME [OPTIONS] A self-updating wrapper script for the Certbot ACME client. When run, updates diff --git a/letsencrypt-nginx/setup.py b/letsencrypt-nginx/setup.py index 3acfa9bb8..25db12a47 100644 --- a/letsencrypt-nginx/setup.py +++ b/letsencrypt-nginx/setup.py @@ -16,7 +16,7 @@ here = os.path.abspath(os.path.dirname(__file__)) readme = read_file(os.path.join(here, 'README.rst')) -version = '0.7.0' +version = '0.8.0.dev0' # This package is a simple shim around certbot-nginx diff --git a/letsencrypt/setup.py b/letsencrypt/setup.py index fd6544265..4541e85fe 100644 --- a/letsencrypt/setup.py +++ b/letsencrypt/setup.py @@ -20,7 +20,7 @@ readme = read_file(os.path.join(here, 'README.rst')) install_requires = ['certbot'] -version = '0.7.0' +version = '0.8.0.dev0' setup( From b627f643772444b965f0e03b0da19f99b5d7c4f5 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Fri, 27 May 2016 15:04:59 -0700 Subject: [PATCH 234/396] Fix stray merge error --- certbot/tests/cli_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certbot/tests/cli_test.py b/certbot/tests/cli_test.py index 09a882c89..671da16f0 100644 --- a/certbot/tests/cli_test.py +++ b/certbot/tests/cli_test.py @@ -177,7 +177,7 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods import platform plat = platform.platform() if "linux" in plat.lower(): - self.assertTrue(le_util.get_os_info_ua() in ua) + self.assertTrue(util.get_os_info_ua() in ua) with mock.patch('certbot.main.client.acme_client.ClientNetwork') as acme_net: ua = "bandersnatch" From 24915f6d008e50c2a242e9ac7e752ad28cb0f623 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Fri, 27 May 2016 16:45:12 -0700 Subject: [PATCH 235/396] Lint --- certbot/tests/util_test.py | 1 - 1 file changed, 1 deletion(-) diff --git a/certbot/tests/util_test.py b/certbot/tests/util_test.py index 4cbc9b663..8e1b330ed 100644 --- a/certbot/tests/util_test.py +++ b/certbot/tests/util_test.py @@ -10,7 +10,6 @@ import unittest import mock import six -import certbot from certbot import errors from certbot.tests import test_util From 4a8f71277c51d8cb26970f98bdbbc8ee67396397 Mon Sep 17 00:00:00 2001 From: Amjad Mashaal Date: Fri, 20 May 2016 21:25:44 +0200 Subject: [PATCH 236/396] Limiting tox envlist to really needed tests --- docs/contributing.rst | 3 +++ tox.ini | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/contributing.rst b/docs/contributing.rst index 3318ec103..9ceb0fbdd 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -71,6 +71,9 @@ The following tools are there to help you: experimental, non-production Apache2 install on them. ``tox -e apacheconftest`` can be used to run those specific Apache conf tests. +- ``tox --skip-missing-interpreters`` runs tox while ignoring missing versions + of Python needed for running the tests. + - ``tox -e py27``, ``tox -e py26`` etc, run unit tests for specific Python versions. diff --git a/tox.ini b/tox.ini index 5c88dfd21..4067bada9 100644 --- a/tox.ini +++ b/tox.ini @@ -4,7 +4,7 @@ [tox] skipsdist = true -envlist = py{26,27,33,34,35},py{26,27}-oldest,cover,lint +envlist = py{26,33,34,35},cover,lint # nosetest -v => more verbose output, allows to detect busy waiting # loops, especially on Travis From 928ea2f5cb7edf22d77ff3bc4bbca1b36c92b7cd Mon Sep 17 00:00:00 2001 From: Amjad Mashaal Date: Sat, 28 May 2016 02:18:49 +0200 Subject: [PATCH 237/396] Fixing minute problems with code --- certbot/main.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/certbot/main.py b/certbot/main.py index 235d7c3ff..9c8849cfe 100644 --- a/certbot/main.py +++ b/certbot/main.py @@ -1,6 +1,7 @@ """Certbot main entry point.""" from __future__ import print_function import atexit +import dialog import functools import logging.handlers import os @@ -623,8 +624,7 @@ def _handle_exception(exc_type, exc_value, trace, config): # Here we're passing a client or ACME error out to the client at the shell # Tell the user a bit about what happened, without overwhelming # them with a full traceback - from dialog import DialogError - if issubclass(exc_type, DialogError): + if issubclass(exc_type, dialog.error): err = exc_value.complete_message() else: err = traceback.format_exception_only(exc_type, exc_value)[0] From b24768751753c6ad2aead5167b5ca3eaf345b155 Mon Sep 17 00:00:00 2001 From: Chris Lamb Date: Sat, 28 May 2016 10:45:57 +0100 Subject: [PATCH 238/396] Don't call os.pid() the default kwargs to atexit_print_messages Whilst it has no side-effects, this results in non-reproducible output when generating the certbot documentation: http://i.imgur.com/Q6VGi9S.jpg Signed-off-by: Chris Lamb --- certbot/reporter.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/certbot/reporter.py b/certbot/reporter.py index d509cb0b8..fe34ece54 100644 --- a/certbot/reporter.py +++ b/certbot/reporter.py @@ -16,6 +16,11 @@ from certbot import le_util logger = logging.getLogger(__name__) +# Store the pid of the process that first imported this module so that +# atexit_print_messages side-effects such as error reporting can be limited to +# this process and not any fork()'d children. +INITIAL_PID = os.getpid() + @zope.interface.implementer(interfaces.IReporter) class Reporter(object): @@ -55,12 +60,14 @@ class Reporter(object): self.messages.put(self._msg_type(priority, msg, on_crash)) logger.info("Reporting to user: %s", msg) - def atexit_print_messages(self, pid=os.getpid()): + def atexit_print_messages(self, pid=None): """Function to be registered with atexit to print messages. :param int pid: Process ID """ + if pid is None: + pid = INITIAL_PID # This ensures that messages are only printed from the process that # created the Reporter. if pid == os.getpid(): From 2e12fda802627b91953e0bc5397a7aa7fe4cc544 Mon Sep 17 00:00:00 2001 From: Shaun Cummiskey Date: Sat, 28 May 2016 16:08:28 -0500 Subject: [PATCH 239/396] Spelling correction --- certbot/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certbot/main.py b/certbot/main.py index 933a102d8..96c0221a2 100644 --- a/certbot/main.py +++ b/certbot/main.py @@ -292,7 +292,7 @@ def _report_new_cert(config, cert_path, fullchain_path): msg = ('Congratulations! Your certificate {0} been saved at {1}.' ' Your cert will expire on {2}. To obtain a new or tweaked version of this ' 'certificate in the future, simply run {3} again{4}. ' - 'To non-interactively renew *all* of your ceriticates, run "{3} renew"' + 'To non-interactively renew *all* of your certificates, run "{3} renew"' .format(and_chain, path, expiry, cli.cli_command, verbswitch)) reporter_util.add_message(msg, reporter_util.MEDIUM_PRIORITY) From 2a9e190cf22501236f6a0fbc5978367278dbc9b8 Mon Sep 17 00:00:00 2001 From: LeCoyote Date: Sun, 29 May 2016 15:54:01 +0400 Subject: [PATCH 240/396] Changed Gentoo os_info string See bug #3091 --- certbot-apache/certbot_apache/constants.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certbot-apache/certbot_apache/constants.py b/certbot-apache/certbot_apache/constants.py index ffbedc90b..f73004a81 100644 --- a/certbot-apache/certbot_apache/constants.py +++ b/certbot-apache/certbot_apache/constants.py @@ -80,7 +80,7 @@ CLI_DEFAULTS = { "red hat enterprise linux server": CLI_DEFAULTS_CENTOS, "rhel": CLI_DEFAULTS_CENTOS, "amazon": CLI_DEFAULTS_CENTOS, - "gentoo base system": CLI_DEFAULTS_GENTOO, + "gentoo": CLI_DEFAULTS_GENTOO, "darwin": CLI_DEFAULTS_DARWIN, } """CLI defaults.""" From d21f8fa657cf4fbea47da47f1dc0bb5e1c9e594a Mon Sep 17 00:00:00 2001 From: Marius Gedminas Date: Fri, 20 May 2016 08:50:00 +0300 Subject: [PATCH 241/396] Add --disable-hook-validation As discussed in #3020. --- certbot/cli.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/certbot/cli.py b/certbot/cli.py index bcb7785c5..f3decec05 100644 --- a/certbot/cli.py +++ b/certbot/cli.py @@ -356,7 +356,8 @@ class HelpfulArgumentParser(object): " {0} conflicts with dialog_mode").format(arg) ) - hooks.validate_hooks(parsed_args) + if parsed_args.validate_hooks: + hooks.validate_hooks(parsed_args) return parsed_args @@ -792,6 +793,14 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): "For this command, the shell variable $RENEWED_LINEAGE will point to the" "config live subdirectory containing the new certs and keys; the shell variable " "$RENEWED_DOMAINS will contain a space-delimited list of renewed cert domains") + helpful.add( + "renew", "--disable-hook-validation", + action='store_false', dest='validate_hooks', default=True, + help="Ordinarily the commands specified for --pre-hook/--post-hook/--renew-hook" + " will be checked for validity, to see if the programs being run are in the $PATH," + " so that mistakes can be caught early, even when the hooks aren't being run just yet." + " The validation is rather simplistic and fails if you use more advanced" + " shell constructs, so you can use this switch to disable it.") helpful.add_deprecated_argument("--agree-dev-preview", 0) From b324845a9cc76c8234005f700763b7951945bb01 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Tue, 31 May 2016 15:24:19 -0700 Subject: [PATCH 242/396] fixes #1080 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 4ee56576b..53fde5282 100644 --- a/setup.py +++ b/setup.py @@ -40,7 +40,7 @@ install_requires = [ 'configobj', 'cryptography>=0.7', # load_pem_x509_certificate 'parsedatetime>=1.3', # Calendar.parseDT - 'psutil>=2.1.0', # net_connections introduced in 2.1.0 + 'psutil>=2.2.1', # 2.1.0 for net_connections and 2.2.1 resolves #1080 'PyOpenSSL', 'pyrfc3339', 'python2-pythondialog>=3.2.2rc1', # Debian squeeze support, cf. #280 From c1e4b57d374201e1eb19a32357485df11a368a92 Mon Sep 17 00:00:00 2001 From: Seth Schoen Date: Tue, 31 May 2016 15:53:40 -0700 Subject: [PATCH 243/396] Tiny documentation fixes --- certbot/display/ops.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/certbot/display/ops.py b/certbot/display/ops.py index 16c44a881..a91c2d747 100644 --- a/certbot/display/ops.py +++ b/certbot/display/ops.py @@ -18,7 +18,7 @@ z_util = zope.component.getUtility def get_email(invalid=False, optional=True): """Prompt for valid email address. - :param bool invalid: True if an invalid was provided by the user + :param bool invalid: True if an invalid address was provided by the user :param bool optional: True if the user can use --register-unsafely-without-email to avoid providing an e-mail @@ -32,7 +32,7 @@ def get_email(invalid=False, optional=True): msg = "Enter email address (used for urgent notices and lost key recovery)" unsafe_suggestion = ("\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 " + "but make sure you then backup your account key from " "/etc/letsencrypt/accounts\n\n") if optional: if invalid: From 590d816fa9b027cd1252c664eaa74348a7f65398 Mon Sep 17 00:00:00 2001 From: bmw Date: Tue, 31 May 2016 16:03:42 -0700 Subject: [PATCH 244/396] s/assert_called_once/assert_called_once_with (#3100) --- acme/acme/client_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/acme/acme/client_test.py b/acme/acme/client_test.py index 33e80aab7..a526a0984 100644 --- a/acme/acme/client_test.py +++ b/acme/acme/client_test.py @@ -572,7 +572,7 @@ class ClientNetworkTest(unittest.TestCase): sess = mock.MagicMock() self.net.session = sess del self.net - sess.close.assert_called_once() + sess.close.assert_called_once_with() @mock.patch('acme.client.requests') def test_requests_error_passthrough(self, mock_requests): From 33e905c147ee4a7d731ab128bbac7273c33ec5fe Mon Sep 17 00:00:00 2001 From: Leo Famulari Date: Tue, 31 May 2016 20:37:05 -0400 Subject: [PATCH 245/396] docs: Fix generation of manpage certbot(1). * docs/man/certbot.rst: Fix path to resource file. --- docs/man/certbot.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/man/certbot.rst b/docs/man/certbot.rst index 8fb03db49..d03f3eed4 100644 --- a/docs/man/certbot.rst +++ b/docs/man/certbot.rst @@ -1 +1 @@ -.. literalinclude:: cli-help.txt +.. literalinclude:: ../cli-help.txt From 8d6502a756a8d949fd6016007e61663086102576 Mon Sep 17 00:00:00 2001 From: LeCoyote Date: Thu, 2 Jun 2016 18:17:21 +0400 Subject: [PATCH 246/396] Update constants.py Add strings for Gentoo: one matches platform.linux_distribution(), the other matches contents of /etc/os-release --- certbot-apache/certbot_apache/constants.py | 1 + 1 file changed, 1 insertion(+) diff --git a/certbot-apache/certbot_apache/constants.py b/certbot-apache/certbot_apache/constants.py index f73004a81..9252814c4 100644 --- a/certbot-apache/certbot_apache/constants.py +++ b/certbot-apache/certbot_apache/constants.py @@ -81,6 +81,7 @@ CLI_DEFAULTS = { "rhel": CLI_DEFAULTS_CENTOS, "amazon": CLI_DEFAULTS_CENTOS, "gentoo": CLI_DEFAULTS_GENTOO, + "gentoo base system": CLI_DEFAULTS_GENTOO, "darwin": CLI_DEFAULTS_DARWIN, } """CLI defaults.""" From 8a8a8b776d403f454d3c8b1afcd766e10a91bb23 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Thu, 2 Jun 2016 13:17:41 -0700 Subject: [PATCH 247/396] permanently pin 0.7.0 of letsencrypt in certbot-auto --- letsencrypt-auto-source/letsencrypt-auto | 9 +++------ .../pieces/letsencrypt-auto-requirements.txt | 9 +++------ 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/letsencrypt-auto-source/letsencrypt-auto b/letsencrypt-auto-source/letsencrypt-auto index 2de0652a9..1992c9d47 100755 --- a/letsencrypt-auto-source/letsencrypt-auto +++ b/letsencrypt-auto-source/letsencrypt-auto @@ -713,6 +713,9 @@ zope.interface==4.1.3 \ mock==1.0.1 \ --hash=sha256:b839dd2d9c117c701430c149956918a423a9863b48b09c90e30a6013e7d2f44f \ --hash=sha256:8f83080daa249d036cbccfb8ae5cc6ff007b88d6d937521371afabe7b19badbc +letsencrypt==0.7.0 \ + --hash=sha256:105a5fb107e45bcd0722eb89696986dcf5f08a86a321d6aef25a0c7c63375ade \ + --hash=sha256:c36e532c486a7e92155ee09da54b436a3c420813ec1c590b98f635d924720de9 # THE LINES BELOW ARE EDITED BY THE RELEASE SCRIPT; ADD ALL DEPENDENCIES ABOVE. @@ -725,12 +728,6 @@ certbot==0.7.0 \ certbot-apache==0.7.0 \ --hash=sha256:5ab5ed9b2af6c7db9495ce1491122798e9d0764e3df8f0843d11d89690bf7f88 \ --hash=sha256:1ddbfaf01bcb0b05c0dcc8b2ebd37637f080cf798151e8140c20c9f5fe7bae75 -letsencrypt==0.7.0 \ - --hash=sha256:105a5fb107e45bcd0722eb89696986dcf5f08a86a321d6aef25a0c7c63375ade \ - --hash=sha256:c36e532c486a7e92155ee09da54b436a3c420813ec1c590b98f635d924720de9 -letsencrypt-apache==0.7.0 \ - --hash=sha256:10445980a6afc810325ea22a56e269229999120848f6c0b323b00275696b5c80 \ - --hash=sha256:3f4656088a18e4efea7cd7eb4965e14e8d901f3b64f4691e79cafd0bb91890f0 UNLIKELY_EOF # ------------------------------------------------------------------------- diff --git a/letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt b/letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt index 6405efd78..a4af06076 100644 --- a/letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt +++ b/letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt @@ -175,6 +175,9 @@ zope.interface==4.1.3 \ mock==1.0.1 \ --hash=sha256:b839dd2d9c117c701430c149956918a423a9863b48b09c90e30a6013e7d2f44f \ --hash=sha256:8f83080daa249d036cbccfb8ae5cc6ff007b88d6d937521371afabe7b19badbc +letsencrypt==0.7.0 \ + --hash=sha256:105a5fb107e45bcd0722eb89696986dcf5f08a86a321d6aef25a0c7c63375ade \ + --hash=sha256:c36e532c486a7e92155ee09da54b436a3c420813ec1c590b98f635d924720de9 # THE LINES BELOW ARE EDITED BY THE RELEASE SCRIPT; ADD ALL DEPENDENCIES ABOVE. @@ -187,9 +190,3 @@ certbot==0.7.0 \ certbot-apache==0.7.0 \ --hash=sha256:5ab5ed9b2af6c7db9495ce1491122798e9d0764e3df8f0843d11d89690bf7f88 \ --hash=sha256:1ddbfaf01bcb0b05c0dcc8b2ebd37637f080cf798151e8140c20c9f5fe7bae75 -letsencrypt==0.7.0 \ - --hash=sha256:105a5fb107e45bcd0722eb89696986dcf5f08a86a321d6aef25a0c7c63375ade \ - --hash=sha256:c36e532c486a7e92155ee09da54b436a3c420813ec1c590b98f635d924720de9 -letsencrypt-apache==0.7.0 \ - --hash=sha256:10445980a6afc810325ea22a56e269229999120848f6c0b323b00275696b5c80 \ - --hash=sha256:3f4656088a18e4efea7cd7eb4965e14e8d901f3b64f4691e79cafd0bb91890f0 From 2659ec3188c0effa0b0f0c6387f582ffc656b04a Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Thu, 2 Jun 2016 13:27:52 -0700 Subject: [PATCH 248/396] Stop packaging shim packages --- tools/release.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tools/release.sh b/tools/release.sh index 89a2f5140..c883e3d61 100755 --- a/tools/release.sh +++ b/tools/release.sh @@ -45,7 +45,7 @@ export GPG_TTY=$(tty) PORT=${PORT:-1234} # subpackages to be released -SUBPKGS=${SUBPKGS:-"acme certbot-apache certbot-nginx letsencrypt letsencrypt-apache letsencrypt-nginx"} +SUBPKGS=${SUBPKGS:-"acme certbot-apache certbot-nginx"} subpkgs_modules="$(echo $SUBPKGS | sed s/-/_/g)" # certbot_compatibility_test is not packaged because: # - it is not meant to be used by anyone else than Certbot devs @@ -164,19 +164,19 @@ for module in certbot $subpkgs_modules ; do done # pin pip hashes of the things we just built -for pkg in acme certbot certbot-apache letsencrypt letsencrypt-apache ; do +for pkg in acme certbot certbot-apache ; do echo $pkg==$version \\ pip hash dist."$version/$pkg"/*.{whl,gz} | grep "^--hash" | python2 -c 'from sys import stdin; input = stdin.read(); print " ", input.replace("\n--hash", " \\\n --hash"),' done > /tmp/hashes.$$ deactivate -if ! wc -l /tmp/hashes.$$ | grep -qE "^\s*15 " ; then +if ! wc -l /tmp/hashes.$$ | grep -qE "^\s*9 " ; then echo Unexpected pip hash output exit 1 fi # perform hideous surgery on requirements.txt... -head -n -15 letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt > /tmp/req.$$ +head -n -9 letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt > /tmp/req.$$ cat /tmp/hashes.$$ >> /tmp/req.$$ cp /tmp/req.$$ letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt From dcadcf8d42c6911a1c35fcd08590f693a68c8fef Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Thu, 2 Jun 2016 13:50:30 -0700 Subject: [PATCH 249/396] Release 0.8.0 --- acme/setup.py | 2 +- certbot-apache/setup.py | 2 +- certbot-auto | 29 ++++++++---------- certbot-compatibility-test/setup.py | 2 +- certbot-nginx/setup.py | 2 +- certbot/__init__.py | 2 +- docs/cli-help.txt | 6 ++++ letsencrypt-auto | 29 ++++++++---------- letsencrypt-auto-source/certbot-auto.asc | 14 ++++----- letsencrypt-auto-source/letsencrypt-auto | 20 ++++++------ letsencrypt-auto-source/letsencrypt-auto.sig | Bin 256 -> 256 bytes .../pieces/letsencrypt-auto-requirements.txt | 18 +++++------ 12 files changed, 63 insertions(+), 63 deletions(-) diff --git a/acme/setup.py b/acme/setup.py index c25cb5c00..cf3aa4df1 100644 --- a/acme/setup.py +++ b/acme/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.8.0.dev0' +version = '0.8.0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot-apache/setup.py b/certbot-apache/setup.py index 2a4716db7..d85b33f3c 100644 --- a/certbot-apache/setup.py +++ b/certbot-apache/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.8.0.dev0' +version = '0.8.0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot-auto b/certbot-auto index 5fbef43b1..2de5ff48f 100755 --- a/certbot-auto +++ b/certbot-auto @@ -19,7 +19,7 @@ XDG_DATA_HOME=${XDG_DATA_HOME:-~/.local/share} VENV_NAME="letsencrypt" VENV_PATH=${VENV_PATH:-"$XDG_DATA_HOME/$VENV_NAME"} VENV_BIN="$VENV_PATH/bin" -LE_AUTO_VERSION="0.7.0" +LE_AUTO_VERSION="0.8.0" BASENAME=$(basename $0) USAGE="Usage: $BASENAME [OPTIONS] A self-updating wrapper script for the Certbot ACME client. When run, updates @@ -713,24 +713,21 @@ zope.interface==4.1.3 \ mock==1.0.1 \ --hash=sha256:b839dd2d9c117c701430c149956918a423a9863b48b09c90e30a6013e7d2f44f \ --hash=sha256:8f83080daa249d036cbccfb8ae5cc6ff007b88d6d937521371afabe7b19badbc - -# THE LINES BELOW ARE EDITED BY THE RELEASE SCRIPT; ADD ALL DEPENDENCIES ABOVE. - -acme==0.7.0 \ - --hash=sha256:6e61dba343806ad4cb27af84628152abc9e83a0fa24be6065587d2b46f340d7a \ - --hash=sha256:9f75a1947978402026b741bdee8a18fc5a1cfd539b78e523b7e5f279bf18eeb9 -certbot==0.7.0 \ - --hash=sha256:55604e43d231ac226edefed8dc110d792052095c3d75ad0e4a228ae0989fe5fd \ - --hash=sha256:ad5083d75e16d1ab806802d3a32f34973b6d7adaf083aee87e07a6c1359efe88 -certbot-apache==0.7.0 \ - --hash=sha256:5ab5ed9b2af6c7db9495ce1491122798e9d0764e3df8f0843d11d89690bf7f88 \ - --hash=sha256:1ddbfaf01bcb0b05c0dcc8b2ebd37637f080cf798151e8140c20c9f5fe7bae75 letsencrypt==0.7.0 \ --hash=sha256:105a5fb107e45bcd0722eb89696986dcf5f08a86a321d6aef25a0c7c63375ade \ --hash=sha256:c36e532c486a7e92155ee09da54b436a3c420813ec1c590b98f635d924720de9 -letsencrypt-apache==0.7.0 \ - --hash=sha256:10445980a6afc810325ea22a56e269229999120848f6c0b323b00275696b5c80 \ - --hash=sha256:3f4656088a18e4efea7cd7eb4965e14e8d901f3b64f4691e79cafd0bb91890f0 + +# THE LINES BELOW ARE EDITED BY THE RELEASE SCRIPT; ADD ALL DEPENDENCIES ABOVE. + +acme==0.8.0 \ + --hash=sha256:8561d590e496afb41a8ff2dac389199661d9cd785b1636ae08325771511189af \ + --hash=sha256:dfa86b547628b231f275c7e0efc7a09bec5dfaec866f89f5c5b59b78c14564da +certbot==0.8.0 \ + --hash=sha256:395c5840ff6b75aa51ee6449c86d016c14c5f65a71281e7bcef5feecac6a3293 \ + --hash=sha256:3c3c70b484fb3243a166515adc81ae0401c5d687a2763c75b40df9d8241a4314 +certbot-apache==0.8.0 \ + --hash=sha256:f4d4fc962ecc19646f6745d49c62a265d26e5b2df3acf34ef4865351594156e3 \ + --hash=sha256:cfb211debbcb0d0645c88d7e8bb38c591fca263bfdb5337242c023956055e268 UNLIKELY_EOF # ------------------------------------------------------------------------- diff --git a/certbot-compatibility-test/setup.py b/certbot-compatibility-test/setup.py index 8d2bd925d..15a2af6e2 100644 --- a/certbot-compatibility-test/setup.py +++ b/certbot-compatibility-test/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.8.0.dev0' +version = '0.8.0' install_requires = [ 'certbot=={0}'.format(version), diff --git a/certbot-nginx/setup.py b/certbot-nginx/setup.py index bb8b3414e..a710739f9 100644 --- a/certbot-nginx/setup.py +++ b/certbot-nginx/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.8.0.dev0' +version = '0.8.0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot/__init__.py b/certbot/__init__.py index dc0e2764d..98971584a 100644 --- a/certbot/__init__.py +++ b/certbot/__init__.py @@ -1,4 +1,4 @@ """Certbot client.""" # version number like 1.2.3a0, must have at least 2 parts, like 1.2 -__version__ = '0.8.0.dev0' +__version__ = '0.8.0' diff --git a/docs/cli-help.txt b/docs/cli-help.txt index 4026f1cc8..b25326148 100644 --- a/docs/cli-help.txt +++ b/docs/cli-help.txt @@ -10,6 +10,7 @@ cert. Major SUBCOMMANDS are: install Install a previously obtained cert in a server renew Renew previously obtained certs that are near expiry revoke Revoke a previously obtained certificate + register Perform tasks related to registering with the CA rollback Rollback server configuration changes made during install config_changes Show changes made to server config during installation plugins Display information about installed plugins @@ -53,6 +54,11 @@ optional arguments: to the Subscriber Agreement will still affect you, and will be effective 14 days after posting an update to the web site. (default: False) + --update-registration + With the register verb, indicates that details + associated with an existing registration, such as the + e-mail address, should be updated, rather than + registering a new account. (default: False) -m EMAIL, --email EMAIL Email used for registration and recovery contact. (default: None) diff --git a/letsencrypt-auto b/letsencrypt-auto index 5fbef43b1..2de5ff48f 100755 --- a/letsencrypt-auto +++ b/letsencrypt-auto @@ -19,7 +19,7 @@ XDG_DATA_HOME=${XDG_DATA_HOME:-~/.local/share} VENV_NAME="letsencrypt" VENV_PATH=${VENV_PATH:-"$XDG_DATA_HOME/$VENV_NAME"} VENV_BIN="$VENV_PATH/bin" -LE_AUTO_VERSION="0.7.0" +LE_AUTO_VERSION="0.8.0" BASENAME=$(basename $0) USAGE="Usage: $BASENAME [OPTIONS] A self-updating wrapper script for the Certbot ACME client. When run, updates @@ -713,24 +713,21 @@ zope.interface==4.1.3 \ mock==1.0.1 \ --hash=sha256:b839dd2d9c117c701430c149956918a423a9863b48b09c90e30a6013e7d2f44f \ --hash=sha256:8f83080daa249d036cbccfb8ae5cc6ff007b88d6d937521371afabe7b19badbc - -# THE LINES BELOW ARE EDITED BY THE RELEASE SCRIPT; ADD ALL DEPENDENCIES ABOVE. - -acme==0.7.0 \ - --hash=sha256:6e61dba343806ad4cb27af84628152abc9e83a0fa24be6065587d2b46f340d7a \ - --hash=sha256:9f75a1947978402026b741bdee8a18fc5a1cfd539b78e523b7e5f279bf18eeb9 -certbot==0.7.0 \ - --hash=sha256:55604e43d231ac226edefed8dc110d792052095c3d75ad0e4a228ae0989fe5fd \ - --hash=sha256:ad5083d75e16d1ab806802d3a32f34973b6d7adaf083aee87e07a6c1359efe88 -certbot-apache==0.7.0 \ - --hash=sha256:5ab5ed9b2af6c7db9495ce1491122798e9d0764e3df8f0843d11d89690bf7f88 \ - --hash=sha256:1ddbfaf01bcb0b05c0dcc8b2ebd37637f080cf798151e8140c20c9f5fe7bae75 letsencrypt==0.7.0 \ --hash=sha256:105a5fb107e45bcd0722eb89696986dcf5f08a86a321d6aef25a0c7c63375ade \ --hash=sha256:c36e532c486a7e92155ee09da54b436a3c420813ec1c590b98f635d924720de9 -letsencrypt-apache==0.7.0 \ - --hash=sha256:10445980a6afc810325ea22a56e269229999120848f6c0b323b00275696b5c80 \ - --hash=sha256:3f4656088a18e4efea7cd7eb4965e14e8d901f3b64f4691e79cafd0bb91890f0 + +# THE LINES BELOW ARE EDITED BY THE RELEASE SCRIPT; ADD ALL DEPENDENCIES ABOVE. + +acme==0.8.0 \ + --hash=sha256:8561d590e496afb41a8ff2dac389199661d9cd785b1636ae08325771511189af \ + --hash=sha256:dfa86b547628b231f275c7e0efc7a09bec5dfaec866f89f5c5b59b78c14564da +certbot==0.8.0 \ + --hash=sha256:395c5840ff6b75aa51ee6449c86d016c14c5f65a71281e7bcef5feecac6a3293 \ + --hash=sha256:3c3c70b484fb3243a166515adc81ae0401c5d687a2763c75b40df9d8241a4314 +certbot-apache==0.8.0 \ + --hash=sha256:f4d4fc962ecc19646f6745d49c62a265d26e5b2df3acf34ef4865351594156e3 \ + --hash=sha256:cfb211debbcb0d0645c88d7e8bb38c591fca263bfdb5337242c023956055e268 UNLIKELY_EOF # ------------------------------------------------------------------------- diff --git a/letsencrypt-auto-source/certbot-auto.asc b/letsencrypt-auto-source/certbot-auto.asc index 454cbe598..0255229b0 100644 --- a/letsencrypt-auto-source/certbot-auto.asc +++ b/letsencrypt-auto-source/certbot-auto.asc @@ -1,11 +1,11 @@ -----BEGIN PGP SIGNATURE----- Version: GnuPG v1 -iQEcBAABAgAGBQJXSK5DAAoJEE0XyZXNl3Xyyb4H/Ahy9/8ADDaN5V/O/6kl6gE5 -amQfm8T10EUD8APnNWYrYKBYruDBVvH0KiEcuAEs7q4xE5BaQatlobSnsHfv4AWW -TwInk2lRxYZ++MwwQf3DrqMK5QKfcoVnViZsRpZ8gHMLzsJllRm7R5eaTewO2ViM -KM+yDB3UsquLUvE4d3/hgBl2mXAUwsxLeFreZayvpoTcX2ARnzbtKqMaIBYDYWcx -DewWtDsPrhKFpb2DY06S6JLmEttysUgv+hbKlaVO0yZ8cCUehkzBIGYoeS4chOLq -fonNCzB8u3RtnLEFiPIy0N+A592jbLsqqUkxjammaJq3lH7nitduMLnpvGKt4yc= -=ex1J +iQEcBAABAgAGBQJXUJvwAAoJEE0XyZXNl3XyvKsH/3qn7Xa/GQx3HvB6Io/Csn/E +v1nbUg5RPwvrTyyol8BJ6UrHiJw+gTbUgCAnBkZ7DYKaC8AQmQXVRcWXNALMMTzB +6LpBXjQQ2xrBYamGj70N7KnTM1QmxI96GUQouiHMJVugV4uihKJDjtR8/f2JWKok +ZSox6E4LqC45HzqLWiOqc13TrHbti32Mo8DyC63PBnSwMnypGLK6XcqM0L9Re62W +smoKu1VWKwWZYRYXIQr0dvK4JmVTrIsdASdZkhTC/vc8y4tGkdN0DcF2EHzci6OA +Tx0W+Ao+HM1ZcaaH3BJ1y3kYfT+mlt6o4OaK3UB/wtUzMmVih7l1UeiNkVL0oYk= +=t3L6 -----END PGP SIGNATURE----- diff --git a/letsencrypt-auto-source/letsencrypt-auto b/letsencrypt-auto-source/letsencrypt-auto index 1992c9d47..2de5ff48f 100755 --- a/letsencrypt-auto-source/letsencrypt-auto +++ b/letsencrypt-auto-source/letsencrypt-auto @@ -19,7 +19,7 @@ XDG_DATA_HOME=${XDG_DATA_HOME:-~/.local/share} VENV_NAME="letsencrypt" VENV_PATH=${VENV_PATH:-"$XDG_DATA_HOME/$VENV_NAME"} VENV_BIN="$VENV_PATH/bin" -LE_AUTO_VERSION="0.8.0.dev0" +LE_AUTO_VERSION="0.8.0" BASENAME=$(basename $0) USAGE="Usage: $BASENAME [OPTIONS] A self-updating wrapper script for the Certbot ACME client. When run, updates @@ -719,15 +719,15 @@ letsencrypt==0.7.0 \ # THE LINES BELOW ARE EDITED BY THE RELEASE SCRIPT; ADD ALL DEPENDENCIES ABOVE. -acme==0.7.0 \ - --hash=sha256:6e61dba343806ad4cb27af84628152abc9e83a0fa24be6065587d2b46f340d7a \ - --hash=sha256:9f75a1947978402026b741bdee8a18fc5a1cfd539b78e523b7e5f279bf18eeb9 -certbot==0.7.0 \ - --hash=sha256:55604e43d231ac226edefed8dc110d792052095c3d75ad0e4a228ae0989fe5fd \ - --hash=sha256:ad5083d75e16d1ab806802d3a32f34973b6d7adaf083aee87e07a6c1359efe88 -certbot-apache==0.7.0 \ - --hash=sha256:5ab5ed9b2af6c7db9495ce1491122798e9d0764e3df8f0843d11d89690bf7f88 \ - --hash=sha256:1ddbfaf01bcb0b05c0dcc8b2ebd37637f080cf798151e8140c20c9f5fe7bae75 +acme==0.8.0 \ + --hash=sha256:8561d590e496afb41a8ff2dac389199661d9cd785b1636ae08325771511189af \ + --hash=sha256:dfa86b547628b231f275c7e0efc7a09bec5dfaec866f89f5c5b59b78c14564da +certbot==0.8.0 \ + --hash=sha256:395c5840ff6b75aa51ee6449c86d016c14c5f65a71281e7bcef5feecac6a3293 \ + --hash=sha256:3c3c70b484fb3243a166515adc81ae0401c5d687a2763c75b40df9d8241a4314 +certbot-apache==0.8.0 \ + --hash=sha256:f4d4fc962ecc19646f6745d49c62a265d26e5b2df3acf34ef4865351594156e3 \ + --hash=sha256:cfb211debbcb0d0645c88d7e8bb38c591fca263bfdb5337242c023956055e268 UNLIKELY_EOF # ------------------------------------------------------------------------- diff --git a/letsencrypt-auto-source/letsencrypt-auto.sig b/letsencrypt-auto-source/letsencrypt-auto.sig index e7e6546a25178083f007351bbba85fda56c87bb2..f024577a34a42779e39755635eca4daf6f46ac2a 100644 GIT binary patch literal 256 zcmV+b0ssE80bqRH{`qI)8kxNPxBYvrI}8PM??lS5W%Vrx#G#77ah$EmwK78hzI8fL zpFoy+Z$Ff?%2R#7_m4mldi$VK+OCedjDk7*K$j_4-%~DH(I7GKai+aBwofiSx4Q27 z;SV#~5cO^_zi`sHUA566Xqu@Q&d>p~uV#$A5kiL^Wt5QWl@grCRt$tN>*5-L&1i=@?vr?8a-~$mStKTr+dwJTPqN4;)GA0wY_jCnH7Wesi1I|l7M@y GRi#to9)G0( literal 256 zcmV+b0ssD(3r1$c09~FFRL*(rn=D5xr$W>4tf{yJ0Yd>ifFOs3`#iS_w(uP+p(fG& zTEmPXK+5%ZpiHyaPtmN4i1xvJn%fgm9b2nF4!4kuT8ov;Uq`~S8bQzaVDw Date: Thu, 2 Jun 2016 13:50:37 -0700 Subject: [PATCH 250/396] Bump version to 0.9.0 --- acme/setup.py | 2 +- certbot-apache/setup.py | 2 +- certbot-compatibility-test/setup.py | 2 +- certbot-nginx/setup.py | 2 +- certbot/__init__.py | 2 +- letsencrypt-auto-source/letsencrypt-auto | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/acme/setup.py b/acme/setup.py index cf3aa4df1..ed133e128 100644 --- a/acme/setup.py +++ b/acme/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.8.0' +version = '0.9.0.dev0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot-apache/setup.py b/certbot-apache/setup.py index d85b33f3c..e3dbe4563 100644 --- a/certbot-apache/setup.py +++ b/certbot-apache/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.8.0' +version = '0.9.0.dev0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot-compatibility-test/setup.py b/certbot-compatibility-test/setup.py index 15a2af6e2..fe2c0c9d0 100644 --- a/certbot-compatibility-test/setup.py +++ b/certbot-compatibility-test/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.8.0' +version = '0.9.0.dev0' install_requires = [ 'certbot=={0}'.format(version), diff --git a/certbot-nginx/setup.py b/certbot-nginx/setup.py index a710739f9..62c705b4c 100644 --- a/certbot-nginx/setup.py +++ b/certbot-nginx/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.8.0' +version = '0.9.0.dev0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot/__init__.py b/certbot/__init__.py index 98971584a..34358a5d9 100644 --- a/certbot/__init__.py +++ b/certbot/__init__.py @@ -1,4 +1,4 @@ """Certbot client.""" # version number like 1.2.3a0, must have at least 2 parts, like 1.2 -__version__ = '0.8.0' +__version__ = '0.9.0.dev0' diff --git a/letsencrypt-auto-source/letsencrypt-auto b/letsencrypt-auto-source/letsencrypt-auto index 2de5ff48f..89d345062 100755 --- a/letsencrypt-auto-source/letsencrypt-auto +++ b/letsencrypt-auto-source/letsencrypt-auto @@ -19,7 +19,7 @@ XDG_DATA_HOME=${XDG_DATA_HOME:-~/.local/share} VENV_NAME="letsencrypt" VENV_PATH=${VENV_PATH:-"$XDG_DATA_HOME/$VENV_NAME"} VENV_BIN="$VENV_PATH/bin" -LE_AUTO_VERSION="0.8.0" +LE_AUTO_VERSION="0.9.0.dev0" BASENAME=$(basename $0) USAGE="Usage: $BASENAME [OPTIONS] A self-updating wrapper script for the Certbot ACME client. When run, updates From 76a939ceb3c37419e4e9eccde29c558ae332ad56 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Thu, 2 Jun 2016 16:00:19 -0700 Subject: [PATCH 251/396] Exit if cannot bootstrap --- letsencrypt-auto-source/letsencrypt-auto.template | 1 + 1 file changed, 1 insertion(+) diff --git a/letsencrypt-auto-source/letsencrypt-auto.template b/letsencrypt-auto-source/letsencrypt-auto.template index 43d8bc7e1..07f52147b 100755 --- a/letsencrypt-auto-source/letsencrypt-auto.template +++ b/letsencrypt-auto-source/letsencrypt-auto.template @@ -197,6 +197,7 @@ Bootstrap() { echo "You will need to bootstrap, configure virtualenv, and run pip install manually." echo "Please see https://letsencrypt.readthedocs.org/en/latest/contributing.html#prerequisites" echo "for more info." + exit 1 fi } From c9bdc19851f2bae586c27663747723535fe19a51 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Thu, 2 Jun 2016 16:03:15 -0700 Subject: [PATCH 252/396] Build letsencrypt-auto --- letsencrypt-auto-source/letsencrypt-auto | 1 + 1 file changed, 1 insertion(+) diff --git a/letsencrypt-auto-source/letsencrypt-auto b/letsencrypt-auto-source/letsencrypt-auto index 1992c9d47..a5d699995 100755 --- a/letsencrypt-auto-source/letsencrypt-auto +++ b/letsencrypt-auto-source/letsencrypt-auto @@ -500,6 +500,7 @@ Bootstrap() { echo "You will need to bootstrap, configure virtualenv, and run pip install manually." echo "Please see https://letsencrypt.readthedocs.org/en/latest/contributing.html#prerequisites" echo "for more info." + exit 1 fi } From 6a0c6c85fb7028fdc3793325d40681b7c34f777f Mon Sep 17 00:00:00 2001 From: bmw Date: Thu, 2 Jun 2016 16:42:55 -0700 Subject: [PATCH 253/396] Revert "Use --force-reinstall to fix bad virtualenv package" --- tools/_venv_common.sh | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tools/_venv_common.sh b/tools/_venv_common.sh index dc6ca3dd2..a121af82d 100755 --- a/tools/_venv_common.sh +++ b/tools/_venv_common.sh @@ -18,8 +18,7 @@ virtualenv --no-site-packages $VENV_NAME $VENV_ARGS # Separately install setuptools and pip to make sure following # invocations use latest pip install -U setuptools -# --force-reinstall used to fix broken pip installation on some systems -pip install --force-reinstall -U pip +pip install -U pip pip install "$@" set +x From 091b6a5cdb996d367dacfe7c93e3c2e604235ce9 Mon Sep 17 00:00:00 2001 From: Felix Yan Date: Thu, 2 Jun 2016 23:02:49 -0500 Subject: [PATCH 254/396] Update Arch instructions for the new package name --- docs/using.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/using.rst b/docs/using.rst index f9af07613..8e691f1e8 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -440,7 +440,7 @@ Operating System Packages .. code-block:: shell - sudo pacman -S letsencrypt + sudo pacman -S certbot **Debian** From 6b7a76442e282e7d926c27cc405dcf10a999652f Mon Sep 17 00:00:00 2001 From: Felix Yan Date: Thu, 2 Jun 2016 23:04:14 -0500 Subject: [PATCH 255/396] Update letsencrypt-auto for Arch's new package name --- letsencrypt-auto-source/letsencrypt-auto | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/letsencrypt-auto-source/letsencrypt-auto b/letsencrypt-auto-source/letsencrypt-auto index 1992c9d47..871c64d0c 100755 --- a/letsencrypt-auto-source/letsencrypt-auto +++ b/letsencrypt-auto-source/letsencrypt-auto @@ -476,7 +476,7 @@ Bootstrap() { BootstrapArchCommon else echo "Please use pacman to install letsencrypt packages:" - echo "# pacman -S letsencrypt letsencrypt-apache" + echo "# pacman -S certbot certbot-apache" echo echo "If you would like to use the virtualenv way, please run the script again with the" echo "--debug flag." From ee622618a2cef70b358477e943dff3e375321d7a Mon Sep 17 00:00:00 2001 From: mrstanwell Date: Fri, 3 Jun 2016 00:42:40 -0500 Subject: [PATCH 256/396] Strip "\n" from end of OS version string for OS X. If you don't, it ends up in the UserAgent header and you get an error like: Invalid header value 'CertbotACMEClient/0.8.0 (darwin 10.10.5\n)...' --- certbot/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certbot/util.py b/certbot/util.py index 8507f80d6..35c599737 100644 --- a/certbot/util.py +++ b/certbot/util.py @@ -325,7 +325,7 @@ def get_python_os_info(): os_ver = subprocess.Popen( ["sw_vers", "-productVersion"], stdout=subprocess.PIPE - ).communicate()[0] + ).communicate()[0].rstrip('\n') elif os_type.startswith('freebsd'): # eg "9.3-RC3-p1" os_ver = os_ver.partition("-")[0] From f5c8a63c1896919056f8a7b7dd53eae0252afacb Mon Sep 17 00:00:00 2001 From: Marius Gedminas Date: Fri, 3 Jun 2016 09:44:12 +0300 Subject: [PATCH 257/396] Tests for --disable-hook-validation --- certbot/tests/cli_test.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/certbot/tests/cli_test.py b/certbot/tests/cli_test.py index 671da16f0..adbde1d3e 100644 --- a/certbot/tests/cli_test.py +++ b/certbot/tests/cli_test.py @@ -651,6 +651,18 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods out = stdout.getvalue() self.assertEqual("", out) + def test_renew_hook_validation(self): + self._make_test_renewal_conf('sample-renewal.conf') + args = ["renew", "--dry-run", "--post-hook=no-such-command"] + self._test_renewal_common(True, [], args=args, should_renew=False, + error_expected=True) + + def test_renew_no_hook_validation(self): + self._make_test_renewal_conf('sample-renewal.conf') + args = ["renew", "--dry-run", "--post-hook=no-such-command", + "--disable-hook-validation"] + self._test_renewal_common(True, [], args=args, should_renew=True, + error_expected=False) @mock.patch("certbot.cli.set_by_cli") def test_ancient_webroot_renewal_conf(self, mock_set_by_cli): From 2815361e6381a12dd50e5c0fa88bbd6ec3b8775e Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Fri, 3 Jun 2016 11:12:49 -0700 Subject: [PATCH 258/396] Update the template as well --- letsencrypt-auto-source/letsencrypt-auto.template | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/letsencrypt-auto-source/letsencrypt-auto.template b/letsencrypt-auto-source/letsencrypt-auto.template index 43d8bc7e1..73d819b4a 100755 --- a/letsencrypt-auto-source/letsencrypt-auto.template +++ b/letsencrypt-auto-source/letsencrypt-auto.template @@ -173,7 +173,7 @@ Bootstrap() { BootstrapArchCommon else echo "Please use pacman to install letsencrypt packages:" - echo "# pacman -S letsencrypt letsencrypt-apache" + echo "# pacman -S certbot certbot-apache" echo echo "If you would like to use the virtualenv way, please run the script again with the" echo "--debug flag." From 81cda2903a11b660a88b4b346dd4d9870b9e8260 Mon Sep 17 00:00:00 2001 From: Seth Schoen Date: Fri, 3 Jun 2016 15:30:20 -0700 Subject: [PATCH 259/396] Attempt at putting everything inside Docker --- .../configurators/apache/a2dismod.sh | 14 -- .../configurators/apache/a2enmod.sh | 18 --- .../configurators/apache/apache24.py | 63 -------- .../configurators/apache/common.py | 150 +----------------- .../configurators/common.py | 83 +--------- .../certbot_compatibility_test/test_driver.py | 4 +- .../certbot_compatibility_test/util.py | 10 -- 7 files changed, 5 insertions(+), 337 deletions(-) delete mode 100755 certbot-compatibility-test/certbot_compatibility_test/configurators/apache/a2dismod.sh delete mode 100755 certbot-compatibility-test/certbot_compatibility_test/configurators/apache/a2enmod.sh delete mode 100644 certbot-compatibility-test/certbot_compatibility_test/configurators/apache/apache24.py diff --git a/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/a2dismod.sh b/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/a2dismod.sh deleted file mode 100755 index ca96e216f..000000000 --- a/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/a2dismod.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/bash -# An extremely simplified version of `a2enmod` for disabling modules in the -# httpd docker image. First argument is the server_root and the second is the -# module to be disabled. - -apache_confdir=$1 -module=$2 - -sed -i "/.*"$module".*/d" "$apache_confdir/test.conf" -enabled_conf="$apache_confdir/mods-enabled/"$module".conf" -if [ -e "$enabled_conf" ] -then - rm $enabled_conf -fi diff --git a/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/a2enmod.sh b/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/a2enmod.sh deleted file mode 100755 index 4da9288a2..000000000 --- a/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/a2enmod.sh +++ /dev/null @@ -1,18 +0,0 @@ -#!/bin/bash -# An extremely simplified version of `a2enmod` for enabling modules in the -# httpd docker image. First argument is the Apache ServerRoot which should be -# an absolute path. The second is the module to be enabled, such as `ssl`. - -confdir=$1 -module=$2 - -echo "LoadModule ${module}_module " \ - "/usr/local/apache2/modules/mod_${module}.so" >> "${confdir}/test.conf" -availbase="/mods-available/${module}.conf" -availconf=$confdir$availbase -enabldir="$confdir/mods-enabled" -enablconf="$enabldir/${module}.conf" -if [ -e $availconf -a -d $enabldir -a ! -e $enablconf ] -then - ln -s "..$availbase" $enablconf -fi diff --git a/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/apache24.py b/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/apache24.py deleted file mode 100644 index 927c329ef..000000000 --- a/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/apache24.py +++ /dev/null @@ -1,63 +0,0 @@ -"""Proxies ApacheConfigurator for Apache 2.4 tests""" - -import zope.interface - -from certbot_compatibility_test import errors -from certbot_compatibility_test import interfaces -from certbot_compatibility_test.configurators.apache import common as apache_common - - -# The docker image doesn't actually have the watchdog module, but unless the -# config uses mod_heartbeat or mod_heartmonitor (which aren't installed and -# therefore the config won't be loaded), I believe this isn't a problem -# http://httpd.apache.org/docs/2.4/mod/mod_watchdog.html -STATIC_MODULES = set(["core", "so", "http", "mpm_event", "watchdog"]) - - -SHARED_MODULES = { - "log_config", "logio", "version", "unixd", "access_compat", "actions", - "alias", "allowmethods", "auth_basic", "auth_digest", "auth_form", - "authn_anon", "authn_core", "authn_dbd", "authn_dbm", "authn_file", - "authn_socache", "authnz_ldap", "authz_core", "authz_dbd", "authz_dbm", - "authz_groupfile", "authz_host", "authz_owner", "authz_user", "autoindex", - "buffer", "cache", "cache_disk", "cache_socache", "cgid", "dav", "dav_fs", - "dbd", "deflate", "dir", "dumpio", "env", "expires", "ext_filter", - "file_cache", "filter", "headers", "include", "info", "lbmethod_bybusyness", - "lbmethod_byrequests", "lbmethod_bytraffic", "lbmethod_heartbeat", "ldap", - "log_debug", "macro", "mime", "negotiation", "proxy", "proxy_ajp", - "proxy_balancer", "proxy_connect", "proxy_express", "proxy_fcgi", - "proxy_ftp", "proxy_http", "proxy_scgi", "proxy_wstunnel", "ratelimit", - "remoteip", "reqtimeout", "request", "rewrite", "sed", "session", - "session_cookie", "session_crypto", "session_dbd", "setenvif", - "slotmem_shm", "socache_dbm", "socache_memcache", "socache_shmcb", - "speling", "ssl", "status", "substitute", "unique_id", "userdir", - "vhost_alias"} - - -@zope.interface.implementer(interfaces.IConfiguratorProxy) -class Proxy(apache_common.Proxy): - """Wraps the ApacheConfigurator for Apache 2.4 tests""" - - def __init__(self, args): - """Initializes the plugin with the given command line args""" - super(Proxy, self).__init__(args) - # Running init isn't ideal, but the Docker container needs to survive - # Apache restarts - self.start_docker("bradmw/apache2.4", "init") - - def preprocess_config(self, server_root): - """Prepares the configuration for use in the Docker""" - super(Proxy, self).preprocess_config(server_root) - if self.version[1] != 4: - raise errors.Error("Apache version not 2.4") - - with open(self.test_conf, "a") as f: - for module in self.modules: - if module not in STATIC_MODULES: - if module in SHARED_MODULES: - f.write( - "LoadModule {0}_module /usr/local/apache2/modules/" - "mod_{0}.so\n".format(module)) - else: - raise errors.Error( - "Unsupported module {0}".format(module)) diff --git a/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/common.py b/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/common.py index 9148666fc..7af0ee20e 100644 --- a/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/common.py +++ b/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/common.py @@ -29,58 +29,9 @@ class Proxy(configurators_common.Proxy): super(Proxy, self).__init__(args) self.le_config.apache_le_vhost_ext = "-le-ssl.conf" - self._setup_mock() - self.modules = self.server_root = self.test_conf = self.version = None self._apache_configurator = self._all_names = self._test_names = None - def _setup_mock(self): - """Replaces specific modules with mock.MagicMock""" - mock_subprocess = mock.MagicMock() - mock_subprocess.check_call = self.check_call - mock_subprocess.Popen = self.popen - - mock.patch( - "certbot_apache.configurator.subprocess", - mock_subprocess).start() - mock.patch( - "certbot_apache.parser.subprocess", - mock_subprocess).start() - mock.patch( - "certbot.util.subprocess", - mock_subprocess).start() - mock.patch( - "certbot_apache.configurator.util.exe_exists", - _is_apache_command).start() - - patch = mock.patch( - "certbot_apache.configurator.display_ops.select_vhost") - mock_display = patch.start() - mock_display.side_effect = le_errors.PluginError( - "Unable to determine vhost") - - def check_call(self, command, *args, **kwargs): - """If command is an Apache command, command is executed in the - running docker image. Otherwise, subprocess.check_call is used. - - """ - if _is_apache_command(command): - command = _modify_command(command) - return super(Proxy, self).check_call(command, *args, **kwargs) - else: - return subprocess.check_call(command, *args, **kwargs) - - def popen(self, command, *args, **kwargs): - """If command is an Apache command, command is executed in the - running docker image. Otherwise, subprocess.Popen is used. - - """ - if _is_apache_command(command): - command = _modify_command(command) - return super(Proxy, self).popen(command, *args, **kwargs) - else: - return subprocess.Popen(command, *args, **kwargs) - def __getattr__(self, name): """Wraps the Apache Configurator methods""" method = getattr(self._apache_configurator, name, None) @@ -91,28 +42,19 @@ class Proxy(configurators_common.Proxy): def load_config(self): """Loads the next configuration for the plugin to test""" - if hasattr(self.le_config, "apache_init_script"): - try: - self.check_call([self.le_config.apache_init_script, "stop"]) - except errors.Error: - raise errors.Error( - "Failed to stop previous apache config from running") config = super(Proxy, self).load_config() - self.modules = _get_modules(config) - self.version = _get_version(config) self._all_names, self._test_names = _get_names(config) server_root = _get_server_root(config) with open(os.path.join(config, "config_file")) as f: config_file = os.path.join(server_root, f.readline().rstrip()) - self.test_conf = _create_test_conf(server_root, config_file) self.preprocess_config(server_root) self._prepare_configurator(server_root, config_file) try: - self.check_call("apachectl -d {0} -f {1} -k start".format( + subprocess.check_call("apachectl -d {0} -f {1} -k start".format( server_root, config_file)) except errors.Error: raise errors.Error( @@ -121,34 +63,9 @@ class Proxy(configurators_common.Proxy): return config - def preprocess_config(self, server_root): - # pylint: disable=anomalous-backslash-in-string, no-self-use - """Prepares the configuration for use in the Docker""" - - find = subprocess.Popen( - ["find", server_root, "-type", "f"], - stdout=subprocess.PIPE) - subprocess.check_call([ - "xargs", "sed", "-e", "s/DocumentRoot.*/DocumentRoot " - "\/usr\/local\/apache2\/htdocs/I", - "-e", "s/SSLPassPhraseDialog.*/SSLPassPhraseDialog builtin/I", - "-e", "s/TypesConfig.*/TypesConfig " - "\/usr\/local\/apache2\/conf\/mime.types/I", - "-e", "s/LoadModule/#LoadModule/I", - "-e", "s/SSLCertificateFile.*/SSLCertificateFile " - "\/usr\/local\/apache2\/conf\/empty_cert.pem/I", - "-e", "s/SSLCertificateKeyFile.*/SSLCertificateKeyFile " - "\/usr\/local\/apache2\/conf\/rsa1024_key2.pem/I", - "-i"], stdin=find.stdout) - def _prepare_configurator(self, server_root, config_file): """Prepares the Apache plugin for testing""" self.le_config.apache_server_root = server_root - self.le_config.apache_ctl = "apachectl -d {0} -f {1}".format( - server_root, config_file) - self.le_config.apache_enmod = "a2enmod.sh {0}".format(server_root) - self.le_config.apache_dismod = "a2dismod.sh {0}".format(server_root) - self.le_config.apache_init_script = self.le_config.apache_ctl + " -k" self._apache_configurator = configurator.ApacheConfigurator( config=configuration.NamespaceConfig(self.le_config), @@ -158,7 +75,6 @@ class Proxy(configurators_common.Proxy): def cleanup_from_tests(self): """Performs any necessary cleanup from running plugin tests""" super(Proxy, self).cleanup_from_tests() - mock.patch.stopall() def get_all_names_answer(self): """Returns the set of domain names that the plugin should find""" @@ -183,39 +99,6 @@ class Proxy(configurators_common.Proxy): domain, cert_path, key_path, chain_path, fullchain_path) -def _is_apache_command(command): - """Returns true if command is an Apache command""" - if isinstance(command, list): - command = command[0] - - for apache_command in APACHE_COMMANDS: - if command.startswith(apache_command): - return True - - return False - - -def _modify_command(command): - """Modifies command so configtest works inside the docker image""" - if isinstance(command, list): - for i in xrange(len(command)): - if command[i] == "configtest": - command[i] = "-t" - else: - command = command.replace("configtest", "-t") - - return command - - -def _create_test_conf(server_root, apache_config): - """Creates a test config file and adds it to the Apache config""" - test_conf = os.path.join(server_root, "test.conf") - open(test_conf, "w").close() - subprocess.check_call( - ["sed", "-i", "1iInclude test.conf", apache_config]) - return test_conf - - def _get_server_root(config): """Returns the server root directory in config""" subdirs = [ @@ -251,34 +134,3 @@ def _get_names(config): words[1].find(".") != -1): all_names.add(words[1]) return all_names, non_ip_names - - -def _get_modules(config): - """Returns the list of modules found in module_list""" - modules = [] - with open(os.path.join(config, "modules")) as f: - for line in f: - # Modules list is indented, everything else is headers/footers - if line[0].isspace(): - words = line.split() - # Modules redundantly end in "_module" which we can discard - modules.append(words[0][:-7]) - - return modules - - -def _get_version(config): - """Return version of Apache Server. - - Version is returned as tuple. (ie. 2.4.7 = (2, 4, 7)). Code taken from - the Apache plugin. - - """ - with open(os.path.join(config, "version")) as f: - # Should be on first line of input - matches = APACHE_VERSION_REGEX.findall(f.readline()) - - if len(matches) != 1: - raise errors.Error("Unable to find Apache version") - - return tuple([int(i) for i in matches[0].split(".")]) diff --git a/certbot-compatibility-test/certbot_compatibility_test/configurators/common.py b/certbot-compatibility-test/certbot_compatibility_test/configurators/common.py index 4657883a3..4592eca39 100644 --- a/certbot-compatibility-test/certbot_compatibility_test/configurators/common.py +++ b/certbot-compatibility-test/certbot_compatibility_test/configurators/common.py @@ -4,8 +4,6 @@ import os import shutil import tempfile -import docker - from certbot import constants from certbot_compatibility_test import errors from certbot_compatibility_test import util @@ -18,20 +16,9 @@ class Proxy(object): # pylint: disable=too-many-instance-attributes """A common base for compatibility test configurators""" - _NOT_ADDED_ARGS = True - @classmethod def add_parser_arguments(cls, parser): """Adds command line arguments needed by the plugin""" - if Proxy._NOT_ADDED_ARGS: - group = parser.add_argument_group("docker") - group.add_argument( - "--docker-url", default="unix://var/run/docker.sock", - help="URL of the docker server") - group.add_argument( - "--no-remove", action="store_true", - help="do not delete container on program exit") - Proxy._NOT_ADDED_ARGS = False def __init__(self, args): """Initializes the plugin with the given command line args""" @@ -43,10 +30,8 @@ class Proxy(object): for config in os.listdir(config_dir)] self.args = args - self._docker_client = docker.Client( - base_url=self.args.docker_url, version="auto") - self.http_port, self.https_port = util.get_two_free_ports() - self._container_id = None + self.http_port = 80 + self.https_port = 443 def has_more_configs(self): """Returns true if there are more configs to test""" @@ -54,9 +39,6 @@ class Proxy(object): def cleanup_from_tests(self): """Performs any necessary cleanup from running plugin tests""" - self._docker_client.stop(self._container_id, 0) - if not self.args.no_remove: - self._docker_client.remove_container(self._container_id) def load_config(self): """Returns the next config directory to be tested""" @@ -65,67 +47,6 @@ class Proxy(object): os.makedirs(backup) return self._configs.pop() - def start_docker(self, image_name, command): - """Creates and runs a Docker container with the specified image""" - logger.warning("Pulling Docker image. This may take a minute.") - for line in self._docker_client.pull(image_name, stream=True): - logger.debug(line) - - host_config = docker.utils.create_host_config( - binds={self._temp_dir: {"bind": self._temp_dir, "mode": "rw"}}, - port_bindings={ - 80: ("127.0.0.1", self.http_port), - 443: ("127.0.0.1", self.https_port)},) - container = self._docker_client.create_container( - image_name, command, ports=[80, 443], volumes=self._temp_dir, - host_config=host_config) - if container["Warnings"]: - logger.warning(container["Warnings"]) - self._container_id = container["Id"] - self._docker_client.start(self._container_id) - - def check_call(self, command, *args, **kwargs): - # pylint: disable=unused-argument - """Simulates a call to check_call but executes the command in the - running docker image - - """ - if self.popen(command).returncode: - raise errors.Error( - "{0} exited with a nonzero value".format(command)) - - def popen(self, command, *args, **kwargs): - # pylint: disable=unused-argument - """Simulates a call to Popen but executes the command in the - running docker image - - """ - class SimplePopen(object): - # pylint: disable=too-few-public-methods - """Simplified Popen object""" - def __init__(self, returncode, output): - self.returncode = returncode - self._stdout = output - self._stderr = output - - def communicate(self): - """Returns stdout and stderr""" - return self._stdout, self._stderr - - if isinstance(command, list): - command = " ".join(command) - - returncode, output = self.execute_in_docker(command) - return SimplePopen(returncode, output) - - def execute_in_docker(self, command): - """Executes command inside the running docker image""" - logger.debug("Executing '%s'", command) - exec_id = self._docker_client.exec_create(self._container_id, command) - output = self._docker_client.exec_start(exec_id) - returncode = self._docker_client.exec_inspect(exec_id)["ExitCode"] - return returncode, output - def copy_certs_and_keys(self, cert_path, key_path, chain_path=None): """Copies certs and keys into the temporary directory""" cert_and_key_dir = os.path.join(self._temp_dir, "certs_and_keys") diff --git a/certbot-compatibility-test/certbot_compatibility_test/test_driver.py b/certbot-compatibility-test/certbot_compatibility_test/test_driver.py index 6823dfdab..ad5755d60 100644 --- a/certbot-compatibility-test/certbot_compatibility_test/test_driver.py +++ b/certbot-compatibility-test/certbot_compatibility_test/test_driver.py @@ -25,8 +25,8 @@ from certbot_compatibility_test.configurators.apache import apache24 DESCRIPTION = """ -Tests Certbot plugins against different server configuratons. It is -assumed that Docker is already installed. If no test types is specified, all +Tests Certbot plugins against different server configurations. It is +assumed that Docker is already installed. If no test type is specified, all tests that the plugin supports are performed. """ diff --git a/certbot-compatibility-test/certbot_compatibility_test/util.py b/certbot-compatibility-test/certbot_compatibility_test/util.py index cbce4fb56..570bf1a9e 100644 --- a/certbot-compatibility-test/certbot_compatibility_test/util.py +++ b/certbot-compatibility-test/certbot_compatibility_test/util.py @@ -52,13 +52,3 @@ def extract_configs(configs, parent_dir): raise errors.Error("Unknown configurations file type") return config_dir - - -def get_two_free_ports(): - """Returns two free ports to use for the tests""" - with contextlib.closing(socket.socket()) as sock1: - with contextlib.closing(socket.socket()) as sock2: - sock1.bind(("", 0)) - sock2.bind(("", 0)) - - return sock1.getsockname()[1], sock2.getsockname()[1] From c79924b7712e2bd770b4c1afec2b2ebb21e32a07 Mon Sep 17 00:00:00 2001 From: Seth Schoen Date: Fri, 3 Jun 2016 16:35:10 -0700 Subject: [PATCH 260/396] Work in progress on removing_proxy --- .../configurators/apache/common.py | 15 +++++++++++++-- .../certbot_compatibility_test/test_driver.py | 7 ++++--- certbot-compatibility-test/setup.py | 5 ++--- 3 files changed, 19 insertions(+), 8 deletions(-) diff --git a/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/common.py b/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/common.py index 7af0ee20e..696ef976a 100644 --- a/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/common.py +++ b/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/common.py @@ -9,6 +9,7 @@ import zope.interface from certbot import configuration from certbot import errors as le_errors from certbot_apache import configurator +from certbot_apache import constants from certbot_compatibility_test import errors from certbot_compatibility_test import interfaces from certbot_compatibility_test import util @@ -31,6 +32,11 @@ class Proxy(configurators_common.Proxy): self.modules = self.server_root = self.test_conf = self.version = None self._apache_configurator = self._all_names = self._test_names = None + patch = mock.patch( + "certbot_apache.configurator.display_ops.select_vhost") + mock_display = patch.start() + mock_display.side_effect = le_errors.PluginError( + "Unable to determine vhost") def __getattr__(self, name): """Wraps the Apache Configurator methods""" @@ -50,12 +56,11 @@ class Proxy(configurators_common.Proxy): with open(os.path.join(config, "config_file")) as f: config_file = os.path.join(server_root, f.readline().rstrip()) - self.preprocess_config(server_root) self._prepare_configurator(server_root, config_file) try: subprocess.check_call("apachectl -d {0} -f {1} -k start".format( - server_root, config_file)) + server_root, config_file).split()) except errors.Error: raise errors.Error( "Apache failed to load {0} before tests started".format( @@ -65,8 +70,13 @@ class Proxy(configurators_common.Proxy): def _prepare_configurator(self, server_root, config_file): """Prepares the Apache plugin for testing""" + for k in constants.CLI_DEFAULTS_DEBIAN.keys(): + setattr(self.le_config, "apache_" + k, constants.os_constant(k)) self.le_config.apache_server_root = server_root + # An alias + self.le_config.apache_handle_modules = self.le_config.apache_handle_mods + self._apache_configurator = configurator.ApacheConfigurator( config=configuration.NamespaceConfig(self.le_config), name="apache") @@ -75,6 +85,7 @@ class Proxy(configurators_common.Proxy): def cleanup_from_tests(self): """Performs any necessary cleanup from running plugin tests""" super(Proxy, self).cleanup_from_tests() + mock.patch.stopall() def get_all_names_answer(self): """Returns the set of domain names that the plugin should find""" diff --git a/certbot-compatibility-test/certbot_compatibility_test/test_driver.py b/certbot-compatibility-test/certbot_compatibility_test/test_driver.py index ad5755d60..ce9f590d5 100644 --- a/certbot-compatibility-test/certbot_compatibility_test/test_driver.py +++ b/certbot-compatibility-test/certbot_compatibility_test/test_driver.py @@ -21,7 +21,7 @@ from certbot_compatibility_test import errors from certbot_compatibility_test import util from certbot_compatibility_test import validator -from certbot_compatibility_test.configurators.apache import apache24 +from certbot_compatibility_test.configurators.apache import common DESCRIPTION = """ @@ -31,7 +31,7 @@ tests that the plugin supports are performed. """ -PLUGINS = {"apache": apache24.Proxy} +PLUGINS = {"apache": common.Proxy} logger = logging.getLogger(__name__) @@ -47,6 +47,8 @@ def test_authenticator(plugin, config, temp_dir): "challenge types") return False + import ipdb + ipdb.set_trace() try: responses = plugin.perform(achalls) except le_errors.Error as error: @@ -341,7 +343,6 @@ def main(): temp_dir = tempfile.mkdtemp() plugin = PLUGINS[args.plugin](args) try: - plugin.execute_in_docker("mkdir -p /var/log/apache2") while plugin.has_more_configs(): success = True diff --git a/certbot-compatibility-test/setup.py b/certbot-compatibility-test/setup.py index 8d2bd925d..07dfa1684 100644 --- a/certbot-compatibility-test/setup.py +++ b/certbot-compatibility-test/setup.py @@ -7,9 +7,8 @@ from setuptools import find_packages version = '0.8.0.dev0' install_requires = [ - 'certbot=={0}'.format(version), - 'certbot-apache=={0}'.format(version), - 'docker-py', + 'certbot', + 'certbot-apache', 'requests', 'zope.interface', ] From ec49afb7c05b5523b0eb8fc87f749478086d6a36 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Fri, 3 Jun 2016 16:45:24 -0700 Subject: [PATCH 261/396] UnspacedList: infrastructure for parsing but hiding nginx whitespace --- certbot-nginx/certbot_nginx/nginxparser.py | 68 +++++++++++++++++++++- 1 file changed, 66 insertions(+), 2 deletions(-) diff --git a/certbot-nginx/certbot_nginx/nginxparser.py b/certbot-nginx/certbot_nginx/nginxparser.py index 1577c7b1e..f9348398a 100644 --- a/certbot-nginx/certbot_nginx/nginxparser.py +++ b/certbot-nginx/certbot_nginx/nginxparser.py @@ -1,4 +1,5 @@ """Very low-level nginx config parser based on pyparsing.""" +import copy import string from pyparsing import ( @@ -26,8 +27,8 @@ class RawNginxParser(object): modifier = Literal("=") | Literal("~*") | Literal("~") | Literal("^~") # rules - comment = Literal('#') + restOfLine() - assignment = (key + Optional(space + value, default=None) + semicolon) + comment = White() + Literal('#') + restOfLine() + assignment = White() + key + Optional(space + value, default=None) + semicolon location_statement = Optional(space + modifier) + Optional(space + location) if_statement = Literal("if") + space + Regex(r"\(.+\)") + space map_statement = Literal("map") + space + Regex(r"\S+") + space + Regex(r"\$\S+") + space @@ -136,3 +137,66 @@ def dump(blocks, _file, indentation=4): """ return _file.write(dumps(blocks, indentation)) + + + +spacey = lambda x: isinstance(x, str) and x.isspace() + +class UnspacedList(list): + """Wrap a list [of lists], making any whitespace entries magically invisible""" + + def __init__(self, list_source): + self.spaced = copy.deepcopy(list(list_source)) + + # Turn self into a version of the source list that has spaces removed + # and all sub-lists also UnspaceList()ed + list.__init__(self, list_source) + for i, entry in reversed(list(enumerate(self))): + if isinstance(entry, list): + list.__setitem__(self, i, UnspacedList(entry)) + elif spacey(entry): + list.__delitem__(self, i) + + def insert(self, i, x): + self.spaced.insert(i + self._spaces_before(i), x) + list.insert(self, i, x) + + def append(self, x): + self.spaced.append(x) + list.append(self, x) + + def extend(self, x): + self.spaced.extend(x) + list.extend(self, x) + + def __add__(self, other): + if hasattr(other, "spaced"): + # If the thing added to us is an UnspacedList, use its spaced form + self.spaced.__add__(other.spaced) + else: + self.spaced.__add__(other) + list.__add__(self, other) + + def __setitem__(self, i, value): + self.spaced.__setitem__(i + self._spaces_before(i), value) + list.__setitem__(self, i, value) + + def __delitem__(self, i): + self.spaced.__delitem__(i + self._spaces_before(i)) + list.__delitem__(self, i) + + def _spaces_before(self, idx): + "Count the number of spaces in the spaced list before pos idx in the spaceless one" + spaces = 0 + pos = 0 + while idx != -1: + if spacey(self.spaced[pos]): + spaces += 1 + else: + idx -= 1 + pos += 1 + return spaces + + def with_spaces(self): + return self.spaced + From 91cd19158e4f080a5f013b2c8af772f9d7004157 Mon Sep 17 00:00:00 2001 From: Jacob Hoffman-Andrews Date: Sat, 4 Jun 2016 22:53:51 -0700 Subject: [PATCH 262/396] Improve user experience for linting. Don't run pep8 for directories that we don't actually enforce pep8 on. Install dependencies with -q. Don't print reports, they make it hard to find the actual errors. Remove deprecated fields from acme .pylintrc, they cause unnecessary messages about deprecation. --- acme/.pylintrc | 6 ------ pep8.travis.sh | 12 ------------ tox.ini | 14 +++++++------- 3 files changed, 7 insertions(+), 25 deletions(-) diff --git a/acme/.pylintrc b/acme/.pylintrc index d0d150631..06bb2a01f 100644 --- a/acme/.pylintrc +++ b/acme/.pylintrc @@ -21,12 +21,6 @@ persistent=yes # usually to register additional checkers. load-plugins=linter_plugin -# DEPRECATED -include-ids=no - -# DEPRECATED -symbols=no - # Use multiple processes to speed up Pylint. jobs=1 diff --git a/pep8.travis.sh b/pep8.travis.sh index c13547a78..cadea8489 100755 --- a/pep8.travis.sh +++ b/pep8.travis.sh @@ -2,16 +2,4 @@ set -e # Fail fast -# PEP8 is not ignored in ACME pep8 --config=acme/.pep8 acme - -pep8 \ - setup.py \ - certbot \ - certbot-apache \ - certbot-nginx \ - certbot-compatibility-test \ - letshelp-certbot \ - || echo "PEP8 checking failed, but it's ignored in Travis" - -# echo exits with 0 diff --git a/tox.ini b/tox.ini index 5c88dfd21..cb625ba8d 100644 --- a/tox.ini +++ b/tox.ini @@ -64,14 +64,14 @@ basepython = python2.7 # duplicate code checking; if one of the commands fails, others will # continue, but tox return code will reflect previous error commands = - pip install -e acme[dev] -e .[dev] -e certbot-apache -e certbot-nginx -e certbot-compatibility-test -e letshelp-certbot + pip install -q -e acme[dev] -e .[dev] -e certbot-apache -e certbot-nginx -e certbot-compatibility-test -e letshelp-certbot ./pep8.travis.sh - pylint --rcfile=.pylintrc certbot - pylint --rcfile=acme/.pylintrc acme/acme - pylint --rcfile=.pylintrc certbot-apache/certbot_apache - pylint --rcfile=.pylintrc certbot-nginx/certbot_nginx - pylint --rcfile=.pylintrc certbot-compatibility-test/certbot_compatibility_test - pylint --rcfile=.pylintrc letshelp-certbot/letshelp_certbot + pylint --reports=n --rcfile=.pylintrc certbot + pylint --reports=n --rcfile=acme/.pylintrc acme/acme + pylint --reports=n --rcfile=.pylintrc certbot-apache/certbot_apache + pylint --reports=n --rcfile=.pylintrc certbot-nginx/certbot_nginx + pylint --reports=n --rcfile=.pylintrc certbot-compatibility-test/certbot_compatibility_test + pylint --reports=n --rcfile=.pylintrc letshelp-certbot/letshelp_certbot [testenv:apacheconftest] #basepython = python2.7 From 08ccc64cd1dc0c852ad4189bbc37aa451e7a611c Mon Sep 17 00:00:00 2001 From: Joona Hoikkala Date: Mon, 6 Jun 2016 12:04:44 +0300 Subject: [PATCH 263/396] Initialize augeas in a new method --- certbot-apache/certbot_apache/augeas_configurator.py | 4 +++- certbot-apache/certbot_apache/configurator.py | 6 ++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/certbot-apache/certbot_apache/augeas_configurator.py b/certbot-apache/certbot_apache/augeas_configurator.py index 12753541c..820a58438 100644 --- a/certbot-apache/certbot_apache/augeas_configurator.py +++ b/certbot-apache/certbot_apache/augeas_configurator.py @@ -1,7 +1,6 @@ """Class of Augeas Configurators.""" import logging -import augeas from certbot import errors from certbot import reverter @@ -29,6 +28,9 @@ class AugeasConfigurator(common.Plugin): def __init__(self, *args, **kwargs): super(AugeasConfigurator, self).__init__(*args, **kwargs) + def init_augeas(self): + """ Initialize the actual Augeas instance """ + import augeas self.aug = augeas.Augeas( # specify a directory to load our preferred lens from loadpath=constants.AUGEAS_LENS_DIR, diff --git a/certbot-apache/certbot_apache/configurator.py b/certbot-apache/certbot_apache/configurator.py index e4c06ba7e..9caa4a764 100644 --- a/certbot-apache/certbot_apache/configurator.py +++ b/certbot-apache/certbot_apache/configurator.py @@ -150,6 +150,12 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): :raises .errors.PluginError: If there is any other error """ + # Perform the actual Augeas initialization to be able to react + try: + self.init_augeas() + except ImportError: + raise errors.NoInstallationError("Problem in Augeas installation") + # Verify Apache is installed if not util.exe_exists(constants.os_constant("restart_cmd")[0]): raise errors.NoInstallationError From 7239361342e0001cfc8ad958250564e50e8bd0cb Mon Sep 17 00:00:00 2001 From: Joona Hoikkala Date: Mon, 6 Jun 2016 12:36:54 +0300 Subject: [PATCH 264/396] Test coverage for NoInstallationError --- certbot-apache/certbot_apache/tests/configurator_test.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/certbot-apache/certbot_apache/tests/configurator_test.py b/certbot-apache/certbot_apache/tests/configurator_test.py index a2e39de47..57344bbb6 100644 --- a/certbot-apache/certbot_apache/tests/configurator_test.py +++ b/certbot-apache/certbot_apache/tests/configurator_test.py @@ -55,6 +55,14 @@ class MultipleVhostsTest(util.ApacheTest): self.assertRaises( errors.NoInstallationError, self.config.prepare) + @mock.patch("certbot_apache.augeas_configurator.AugeasConfigurator.init_augeas") + def test_prepare_no_augeas(self, mock_init_augeas): + def side_effect_error(*args, **kwargs): + raise ImportError + mock_init_augeas.side_effect = side_effect_error + self.assertRaises( + errors.NoInstallationError, self.config.prepare) + @mock.patch("certbot_apache.parser.ApacheParser") @mock.patch("certbot_apache.configurator.util.exe_exists") def test_prepare_version(self, mock_exe_exists, _): From e2631322839e4a3b5117fb4581da72fa4c2eddc5 Mon Sep 17 00:00:00 2001 From: Joona Hoikkala Date: Mon, 6 Jun 2016 12:44:49 +0300 Subject: [PATCH 265/396] Refactor and lint fixes --- .../certbot_apache/augeas_configurator.py | 20 +++++++++++-------- .../certbot_apache/tests/configurator_test.py | 4 +++- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/certbot-apache/certbot_apache/augeas_configurator.py b/certbot-apache/certbot_apache/augeas_configurator.py index 820a58438..478efb099 100644 --- a/certbot-apache/certbot_apache/augeas_configurator.py +++ b/certbot-apache/certbot_apache/augeas_configurator.py @@ -28,6 +28,18 @@ class AugeasConfigurator(common.Plugin): def __init__(self, *args, **kwargs): super(AugeasConfigurator, self).__init__(*args, **kwargs) + # Placeholder for augeas + self.aug = None + + self.save_notes = "" + + # See if any temporary changes need to be recovered + # This needs to occur before VirtualHost objects are setup... + # because this will change the underlying configuration and potential + # vhosts + self.reverter = reverter.Reverter(self.config) + self.recovery_routine() + def init_augeas(self): """ Initialize the actual Augeas instance """ import augeas @@ -37,14 +49,6 @@ class AugeasConfigurator(common.Plugin): # Do not save backup (we do it ourselves), do not load # anything by default flags=(augeas.Augeas.NONE | augeas.Augeas.NO_MODL_AUTOLOAD)) - self.save_notes = "" - - # See if any temporary changes need to be recovered - # This needs to occur before VirtualHost objects are setup... - # because this will change the underlying configuration and potential - # vhosts - self.reverter = reverter.Reverter(self.config) - self.recovery_routine() def check_parsing_errors(self, lens): """Verify Augeas can parse all of the lens files. diff --git a/certbot-apache/certbot_apache/tests/configurator_test.py b/certbot-apache/certbot_apache/tests/configurator_test.py index 57344bbb6..e5c09fd1d 100644 --- a/certbot-apache/certbot_apache/tests/configurator_test.py +++ b/certbot-apache/certbot_apache/tests/configurator_test.py @@ -57,7 +57,9 @@ class MultipleVhostsTest(util.ApacheTest): @mock.patch("certbot_apache.augeas_configurator.AugeasConfigurator.init_augeas") def test_prepare_no_augeas(self, mock_init_augeas): - def side_effect_error(*args, **kwargs): + """ Test augeas initialization ImportError """ + def side_effect_error(): + """ Side effect error for the test """ raise ImportError mock_init_augeas.side_effect = side_effect_error self.assertRaises( From 1f6e999153be1347d92eef884a297966c0e15801 Mon Sep 17 00:00:00 2001 From: Joona Hoikkala Date: Mon, 6 Jun 2016 13:10:34 +0300 Subject: [PATCH 266/396] Move recovery_routine() to augeas init --- certbot-apache/certbot_apache/augeas_configurator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certbot-apache/certbot_apache/augeas_configurator.py b/certbot-apache/certbot_apache/augeas_configurator.py index 478efb099..6999120d6 100644 --- a/certbot-apache/certbot_apache/augeas_configurator.py +++ b/certbot-apache/certbot_apache/augeas_configurator.py @@ -38,7 +38,6 @@ class AugeasConfigurator(common.Plugin): # because this will change the underlying configuration and potential # vhosts self.reverter = reverter.Reverter(self.config) - self.recovery_routine() def init_augeas(self): """ Initialize the actual Augeas instance """ @@ -49,6 +48,7 @@ class AugeasConfigurator(common.Plugin): # Do not save backup (we do it ourselves), do not load # anything by default flags=(augeas.Augeas.NONE | augeas.Augeas.NO_MODL_AUTOLOAD)) + self.recovery_routine() def check_parsing_errors(self, lens): """Verify Augeas can parse all of the lens files. From 78ea886a79c3dc3b1c75a5d5eb3dca5abbd15219 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 6 Jun 2016 11:49:36 -0700 Subject: [PATCH 267/396] Fix deploy cert and TLSSNI check --- .../certbot_compatibility_test/test_driver.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/certbot-compatibility-test/certbot_compatibility_test/test_driver.py b/certbot-compatibility-test/certbot_compatibility_test/test_driver.py index ce9f590d5..885eacd92 100644 --- a/certbot-compatibility-test/certbot_compatibility_test/test_driver.py +++ b/certbot-compatibility-test/certbot_compatibility_test/test_driver.py @@ -47,8 +47,6 @@ def test_authenticator(plugin, config, temp_dir): "challenge types") return False - import ipdb - ipdb.set_trace() try: responses = plugin.perform(achalls) except le_errors.Error as error: @@ -63,7 +61,7 @@ def test_authenticator(plugin, config, temp_dir): "Plugin failed to complete %s for %s in %s", type(achalls[i]), achalls[i].domain, config) success = False - elif isinstance(responses[i], challenges.TLSSNI01): + elif isinstance(responses[i], challenges.TLSSNI01Response): verify = functools.partial(responses[i].simple_verify, achalls[i], achalls[i].domain, util.JWK.public_key(), @@ -144,7 +142,7 @@ def test_deploy_cert(plugin, temp_dir, domains): for domain in domains: try: - plugin.deploy_cert(domain, cert_path, util.KEY_PATH) + plugin.deploy_cert(domain, cert_path, util.KEY_PATH, cert_path) except le_errors.Error as error: logger.error("Plugin failed to deploy ceritificate for %s:", domain) logger.exception(error) From 1d3fbe945de27894578fdc79878b4ce5032edaa2 Mon Sep 17 00:00:00 2001 From: Seth Schoen Date: Mon, 6 Jun 2016 12:01:55 -0700 Subject: [PATCH 268/396] Copy config into /etc/apache2 --- .../configurators/apache/common.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/common.py b/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/common.py index 696ef976a..af27f7ed5 100644 --- a/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/common.py +++ b/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/common.py @@ -53,14 +53,15 @@ class Proxy(configurators_common.Proxy): self._all_names, self._test_names = _get_names(config) server_root = _get_server_root(config) - with open(os.path.join(config, "config_file")) as f: - config_file = os.path.join(server_root, f.readline().rstrip()) + # with open(os.path.join(config, "config_file")) as f: + # config_file = os.path.join(server_root, f.readline().rstrip()) + shutil.rmtree("/etc/apache2") + shutil.copytree(server_root, "/etc/apache2", symlinks=True) - self._prepare_configurator(server_root, config_file) + self._prepare_configurator() try: - subprocess.check_call("apachectl -d {0} -f {1} -k start".format( - server_root, config_file).split()) + subprocess.check_call("apachectl -k start".split()) except errors.Error: raise errors.Error( "Apache failed to load {0} before tests started".format( @@ -68,11 +69,10 @@ class Proxy(configurators_common.Proxy): return config - def _prepare_configurator(self, server_root, config_file): + def _prepare_configurator(self): """Prepares the Apache plugin for testing""" for k in constants.CLI_DEFAULTS_DEBIAN.keys(): setattr(self.le_config, "apache_" + k, constants.os_constant(k)) - self.le_config.apache_server_root = server_root # An alias self.le_config.apache_handle_modules = self.le_config.apache_handle_mods From e0bb04fd2550701d5b553a1f1516fd17a096d611 Mon Sep 17 00:00:00 2001 From: Seth Schoen Date: Mon, 6 Jun 2016 12:02:53 -0700 Subject: [PATCH 269/396] Forgot to import shutil --- .../certbot_compatibility_test/configurators/apache/common.py | 1 + 1 file changed, 1 insertion(+) diff --git a/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/common.py b/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/common.py index af27f7ed5..918db5f47 100644 --- a/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/common.py +++ b/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/common.py @@ -1,6 +1,7 @@ """Provides a common base for Apache proxies""" import re import os +import shutil import subprocess import mock From e1f4e22c6d5e158eb213745af571cba29dde3e86 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 6 Jun 2016 12:09:17 -0700 Subject: [PATCH 270/396] Unwrap achall --- .../certbot_compatibility_test/test_driver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certbot-compatibility-test/certbot_compatibility_test/test_driver.py b/certbot-compatibility-test/certbot_compatibility_test/test_driver.py index 885eacd92..165791684 100644 --- a/certbot-compatibility-test/certbot_compatibility_test/test_driver.py +++ b/certbot-compatibility-test/certbot_compatibility_test/test_driver.py @@ -62,7 +62,7 @@ def test_authenticator(plugin, config, temp_dir): type(achalls[i]), achalls[i].domain, config) success = False elif isinstance(responses[i], challenges.TLSSNI01Response): - verify = functools.partial(responses[i].simple_verify, achalls[i], + verify = functools.partial(responses[i].simple_verify, achalls[i].chall, achalls[i].domain, util.JWK.public_key(), host="127.0.0.1", From 111ca657d86e0caf31dbc81f83037a021fd22131 Mon Sep 17 00:00:00 2001 From: Amjad Mashaal Date: Mon, 6 Jun 2016 21:17:02 +0200 Subject: [PATCH 271/396] Adding testing --- certbot/tests/cli_test.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/certbot/tests/cli_test.py b/certbot/tests/cli_test.py index 4ae69e69d..06aba4ab3 100644 --- a/certbot/tests/cli_test.py +++ b/certbot/tests/cli_test.py @@ -882,6 +882,15 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods mock_sys.exit.assert_called_with(''.join( traceback.format_exception_only(KeyboardInterrupt, interrupt))) + # Test dialog errors + + import dialog + exception = dialog.error(message="test message") + main._handle_exception( + dialog.DialogError, exc_value=exception, trace=None, config=None) + error_msg = mock_sys.exit.call_args_list[-1][0][0] + self.assertTrue("test message" in error_msg) + def test_read_file(self): rel_test_path = os.path.relpath(os.path.join(self.tmp_dir, 'foo')) self.assertRaises( From 144dbdd90b0637777163597e338c4872c890e6e3 Mon Sep 17 00:00:00 2001 From: Seth Schoen Date: Mon, 6 Jun 2016 12:23:15 -0700 Subject: [PATCH 272/396] Explain whether tests succeeded or failed overall --- .../certbot_compatibility_test/test_driver.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/certbot-compatibility-test/certbot_compatibility_test/test_driver.py b/certbot-compatibility-test/certbot_compatibility_test/test_driver.py index 165791684..fae2ed2c7 100644 --- a/certbot-compatibility-test/certbot_compatibility_test/test_driver.py +++ b/certbot-compatibility-test/certbot_compatibility_test/test_driver.py @@ -7,6 +7,7 @@ import os import shutil import tempfile import time +import sys import OpenSSL @@ -341,6 +342,7 @@ def main(): temp_dir = tempfile.mkdtemp() plugin = PLUGINS[args.plugin](args) try: + overall_success = True while plugin.has_more_configs(): success = True @@ -359,10 +361,18 @@ def main(): if success: logger.info("All tests on %s succeeded", config) else: + overall_success = False logger.error("Tests on %s failed", config) finally: plugin.cleanup_from_tests() + if overall_success: + logger.warn("All compatibility tests succeeded") + sys.exit(0) + else: + logger.warn("One or more compatibility tests failed") + sys.exit(1) + if __name__ == "__main__": main() From 8723bded727a9901314187427ab0e07fbd43b586 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 6 Jun 2016 14:17:11 -0700 Subject: [PATCH 273/396] Add extra saves for apache plugin --- .../certbot_compatibility_test/test_driver.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/certbot-compatibility-test/certbot_compatibility_test/test_driver.py b/certbot-compatibility-test/certbot_compatibility_test/test_driver.py index fae2ed2c7..38abffb18 100644 --- a/certbot-compatibility-test/certbot_compatibility_test/test_driver.py +++ b/certbot-compatibility-test/certbot_compatibility_test/test_driver.py @@ -144,6 +144,7 @@ def test_deploy_cert(plugin, temp_dir, domains): for domain in domains: try: plugin.deploy_cert(domain, cert_path, util.KEY_PATH, cert_path) + plugin.save() # Needed by the Apache plugin except le_errors.Error as error: logger.error("Plugin failed to deploy ceritificate for %s:", domain) logger.exception(error) @@ -178,6 +179,7 @@ def test_enhancements(plugin, domains): for domain in domains: try: plugin.enhance(domain, "redirect") + plugin.save() # Needed by the Apache plugin except le_errors.PluginError as error: # Don't immediately fail because a redirect may already be enabled logger.warning("Plugin failed to enable redirect for %s:", domain) From 1c1816fb4a49e97b99300d50be00df5f41bb508d Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 6 Jun 2016 14:35:28 -0700 Subject: [PATCH 274/396] Update tarball --- .../testdata/configs.tar.gz | Bin 101287 -> 100286 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/certbot-compatibility-test/certbot_compatibility_test/testdata/configs.tar.gz b/certbot-compatibility-test/certbot_compatibility_test/testdata/configs.tar.gz index 7323acf747643887d8883f3ef74437d811032b2d..05f7f4f9bc54975c0f575ebd80613c9e419dc3b2 100644 GIT binary patch literal 100286 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

>+7bOwi=X@0cTd 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 z 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 literal 101287 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

>PD40$S_DOkt_8cFeaIjOMKd6SRvO4frMW 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 YDNSigQ<~D0rhMh{f548beE?_+0L2q{f&c&j From 5f8964a6a854ba3f1afd002b3062f52d0eea2aee Mon Sep 17 00:00:00 2001 From: Seth Schoen Date: Mon, 6 Jun 2016 14:48:49 -0700 Subject: [PATCH 275/396] Fix typo --- .../certbot_compatibility_test/configurators/apache/common.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/common.py b/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/common.py index 918db5f47..ed3d9d67a 100644 --- a/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/common.py +++ b/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/common.py @@ -118,7 +118,7 @@ def _get_server_root(config): if os.path.isdir(os.path.join(config, name))] if len(subdirs) != 1: - errors.Error("Malformed configuration directiory {0}".format(config)) + errors.Error("Malformed configuration directory {0}".format(config)) return os.path.join(config, subdirs[0].rstrip()) From e826de5db70823594d2c9c286a953de5fc656c11 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 6 Jun 2016 14:52:30 -0700 Subject: [PATCH 276/396] Don't change line endings on a tarball --- .gitattributes | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitattributes b/.gitattributes index 5eee84cce..8c41b686e 100644 --- a/.gitattributes +++ b/.gitattributes @@ -5,3 +5,4 @@ *.jpeg binary *.jpg binary *.png binary +*.gz binary From ce378cec216d73c8f20ee3d34981089a1d842829 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 6 Jun 2016 14:53:12 -0700 Subject: [PATCH 277/396] Try updating tarball again --- .../testdata/configs.tar.gz | Bin 100286 -> 100288 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/certbot-compatibility-test/certbot_compatibility_test/testdata/configs.tar.gz b/certbot-compatibility-test/certbot_compatibility_test/testdata/configs.tar.gz index 05f7f4f9bc54975c0f575ebd80613c9e419dc3b2..9b819d0c7b2d8178d3aad1bb25aeb32a4efae76c 100644 GIT binary patch delta 31 lcmdnj&vu}njZMCrgJIK$jcg)vjJ%trCjhE{3hDp= delta 27 jcmX@m&$h3hjZMCrgJH+(jcg)vo2BLA7H+QE?{)$JjvETn From 092173c60892ee79d50fad19fc1ff6b05a27b852 Mon Sep 17 00:00:00 2001 From: Noah Swartz Date: Mon, 6 Jun 2016 17:05:51 -0700 Subject: [PATCH 278/396] fix broken link in contributing.rst (#3130) --- docs/contributing.rst | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/contributing.rst b/docs/contributing.rst index 3318ec103..267d466e4 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -266,8 +266,7 @@ with the core upstream source code. An example is provided in it with any necessary API changes. .. _`setuptools entry points`: - https://pythonhosted.org/setuptools/setuptools.html#dynamic-discovery-of-services-and-plugins - + http://setuptools.readthedocs.io/en/latest/pkg_resources.html#entry-points .. _coding-style: From ac220976f1032a26d1801da2528ffd554e0e400d Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Tue, 7 Jun 2016 14:46:49 -0700 Subject: [PATCH 279/396] work in progress --- acme/acme/errors.py | 2 +- certbot-nginx/certbot_nginx/nginxparser.py | 52 ++++++++++++++++++---- 2 files changed, 45 insertions(+), 9 deletions(-) diff --git a/acme/acme/errors.py b/acme/acme/errors.py index 77d47c522..ca2ab1874 100644 --- a/acme/acme/errors.py +++ b/acme/acme/errors.py @@ -49,7 +49,7 @@ class MissingNonce(NonceError): def __str__(self): return ('Server {0} response did not include a replay ' - 'nonce, headers: {1}'.format( + 'nonce, headers: {1}\n(This may be a service outage)'.format( self.response.request.method, self.response.headers)) diff --git a/certbot-nginx/certbot_nginx/nginxparser.py b/certbot-nginx/certbot_nginx/nginxparser.py index f9348398a..7b39234dc 100644 --- a/certbot-nginx/certbot_nginx/nginxparser.py +++ b/certbot-nginx/certbot_nginx/nginxparser.py @@ -53,8 +53,44 @@ class RawNginxParser(object): """Returns the parsed tree as a list.""" return self.parse().asList() - class RawNginxDumper(object): + # pylint: disable=too-few-public-methods + """A class that dumps nginx configuration from the provided tree.""" + def __init__(self, blocks, indentation=0): + self.blocks = blocks + self.indentation = indentation + + def __iter__(self, blocks=None, current_indent=0, spacer=''): + """Iterates the dumped nginx content.""" + blocks = blocks or self.blocks + for key, values in blocks.spaced: + #indentation = spacer * current_indent + if isinstance(key, list): + yield "".join(key) + ' {' + + for parameter in values: + dumped = self.__iter__([parameter], current_indent + self.indentation) + for line in dumped: + yield line + + yield '}' + else: + if key == '#': + yield key + values + else: + if values is None: + yield key + ';' + else: + yield key + values + ';' + + def __str__(self): + """Return the parsed block as a string.""" + return '\n'.join(self) + '\n' + + + + +class OldRawNginxDumper(object): # pylint: disable=too-few-public-methods """A class that dumps nginx configuration from the provided tree.""" def __init__(self, blocks, indentation=4): @@ -102,7 +138,7 @@ def loads(source): :rtype: list """ - return RawNginxParser(source).as_list() + return UnspacedList(RawNginxParser(source).as_list()) def load(_file): @@ -113,13 +149,13 @@ def load(_file): :rtype: list """ - return loads(_file.read()) + return UnspacedList(loads(_file.read())) def dumps(blocks, indentation=4): """Dump to a string. - :param list block: The parsed tree + :param UnspacedList block: The parsed tree :param int indentation: The number of spaces to indent :rtype: str @@ -130,7 +166,7 @@ def dumps(blocks, indentation=4): def dump(blocks, _file, indentation=4): """Dump to a file. - :param list block: The parsed tree + :param UnspacedList block: The parsed tree :param file _file: The file to dump to :param int indentation: The number of spaces to indent :rtype: NoneType @@ -149,14 +185,14 @@ class UnspacedList(list): self.spaced = copy.deepcopy(list(list_source)) # Turn self into a version of the source list that has spaces removed - # and all sub-lists also UnspaceList()ed + # and all sub-lists also UnspacedList()ed list.__init__(self, list_source) for i, entry in reversed(list(enumerate(self))): if isinstance(entry, list): list.__setitem__(self, i, UnspacedList(entry)) elif spacey(entry): list.__delitem__(self, i) - + def insert(self, i, x): self.spaced.insert(i + self._spaces_before(i), x) list.insert(self, i, x) @@ -176,7 +212,7 @@ class UnspacedList(list): else: self.spaced.__add__(other) list.__add__(self, other) - + def __setitem__(self, i, value): self.spaced.__setitem__(i + self._spaces_before(i), value) list.__setitem__(self, i, value) From 8aa1d85991daa33dc22172f4535ed65606299a85 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Tue, 7 Jun 2016 16:25:08 -0700 Subject: [PATCH 280/396] Move mageia bootstrap script --- .../pieces/bootstrappers/mageia_common.sh | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename bootstrap/_mageia_common.sh => letsencrypt-auto-source/pieces/bootstrappers/mageia_common.sh (100%) diff --git a/bootstrap/_mageia_common.sh b/letsencrypt-auto-source/pieces/bootstrappers/mageia_common.sh similarity index 100% rename from bootstrap/_mageia_common.sh rename to letsencrypt-auto-source/pieces/bootstrappers/mageia_common.sh From 1c363716a086ec4dfb3b0c3143e915a75efd106f Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Tue, 7 Jun 2016 16:33:04 -0700 Subject: [PATCH 281/396] Wrap mageia bootstrap script in bash function --- .../pieces/bootstrappers/mageia_common.sh | 45 +++++++++---------- 1 file changed, 22 insertions(+), 23 deletions(-) mode change 100755 => 100644 letsencrypt-auto-source/pieces/bootstrappers/mageia_common.sh diff --git a/letsencrypt-auto-source/pieces/bootstrappers/mageia_common.sh b/letsencrypt-auto-source/pieces/bootstrappers/mageia_common.sh old mode 100755 new mode 100644 index 9a4606c9d..d6651574a --- a/letsencrypt-auto-source/pieces/bootstrappers/mageia_common.sh +++ b/letsencrypt-auto-source/pieces/bootstrappers/mageia_common.sh @@ -1,24 +1,23 @@ -#!/bin/sh +BootstrapMageiaCommon() { + if ! $SUDO urpmi --force \ + python \ + libpython-devel \ + python-virtualenv + then + echo "Could not install Python dependencies. Aborting bootstrap!" + exit 1 + fi -# Tested on mageia 5 x86_64 -if ! urpmi --force \ - python \ - libpython-devel \ - python-virtualenv -then - echo "Could not install Python dependencies. Aborting bootstrap!" - exit 1 -fi - -if ! urpmi --force \ - git \ - gcc \ - cdialog \ - python-augeas \ - libopenssl-devel \ - libffi-devel \ - rootcerts -then - echo "Could not install additional dependencies. Aborting bootstrap!" - exit 1 -fi + if ! $SUDO urpmi --force \ + git \ + gcc \ + cdialog \ + python-augeas \ + libopenssl-devel \ + libffi-devel \ + rootcerts + then + echo "Could not install additional dependencies. Aborting bootstrap!" + exit 1 + fi +} From 9be5f7d7d961cf1def5872acec9cb9bc41e11016 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Tue, 7 Jun 2016 17:17:17 -0700 Subject: [PATCH 282/396] Further WIP --- certbot-nginx/certbot_nginx/nginxparser.py | 99 +++++++++++++++++++--- certbot-nginx/certbot_nginx/parser.py | 2 +- 2 files changed, 90 insertions(+), 11 deletions(-) diff --git a/certbot-nginx/certbot_nginx/nginxparser.py b/certbot-nginx/certbot_nginx/nginxparser.py index 7b39234dc..8c2ba197e 100644 --- a/certbot-nginx/certbot_nginx/nginxparser.py +++ b/certbot-nginx/certbot_nginx/nginxparser.py @@ -1,6 +1,7 @@ """Very low-level nginx config parser based on pyparsing.""" import copy import string +import sys from pyparsing import ( Literal, White, Word, alphanums, CharsNotIn, Forward, Group, @@ -18,6 +19,7 @@ class RawNginxParser(object): right_bracket = Literal("}").suppress() semicolon = Literal(";").suppress() space = White().suppress() + keepSpace = Optional(White()) key = Word(alphanums + "_/+-.") # Matches anything that is not a special character AND any chars in single # or double quotes @@ -27,8 +29,9 @@ class RawNginxParser(object): modifier = Literal("=") | Literal("~*") | Literal("~") | Literal("^~") # rules - comment = White() + Literal('#') + restOfLine() - assignment = White() + key + Optional(space + value, default=None) + semicolon + comment = Literal('#') + restOfLine() + + assignment = keepSpace + key + Optional(space + value, default=None) + semicolon location_statement = Optional(space + modifier) + Optional(space + location) if_statement = Literal("if") + space + Regex(r"\(.+\)") + space map_statement = Literal("map") + space + Regex(r"\S+") + space + Regex(r"\$\S+") + space @@ -53,6 +56,51 @@ class RawNginxParser(object): """Returns the parsed tree as a list.""" return self.parse().asList() +class OldRawNginxParser(object): + # pylint: disable=expression-not-assigned + """A class that parses nginx configuration with pyparsing.""" + + # constants + left_bracket = Literal("{").suppress() + right_bracket = Literal("}").suppress() + semicolon = Literal(";").suppress() + space = White().suppress() + key = Word(alphanums + "_/+-.") + # Matches anything that is not a special character AND any chars in single + # or double quotes + value = Regex(r"((\".*\")?(\'.*\')?[^\{\};,]?)+") + location = CharsNotIn("{};," + string.whitespace) + # modifier for location uri [ = | ~ | ~* | ^~ ] + modifier = Literal("=") | Literal("~*") | Literal("~") | Literal("^~") + + # rules + comment = Literal("#") + restOfLine() + assignment = (key + Optional(space + value, default=None) + semicolon) + location_statement = Optional(space + modifier) + Optional(space + location) + if_statement = Literal("if") + space + Regex(r"\(.+\)") + space + map_statement = Literal("map") + space + Regex(r"\S+") + space + Regex(r"\$\S+") + space + block = Forward() + + block << Group( + (Group(key + location_statement) ^ Group(if_statement) ^ Group(map_statement)) + + left_bracket + + Group(ZeroOrMore(Group(comment | assignment) | block)) + + right_bracket) + + script = OneOrMore(Group(comment | assignment) ^ block) + stringEnd + + def __init__(self, source): + self.source = source + + def parse(self): + """Returns the parsed tree.""" + return self.script.parseString(self.source) + + def as_list(self): + """Returns the parsed tree as a list.""" + return self.parse().asList() + + class RawNginxDumper(object): # pylint: disable=too-few-public-methods """A class that dumps nginx configuration from the provided tree.""" @@ -60,13 +108,24 @@ class RawNginxDumper(object): self.blocks = blocks self.indentation = indentation - def __iter__(self, blocks=None, current_indent=0, spacer=''): + def __iter__(self, blocks=None, current_indent=0, spacer=' '): """Iterates the dumped nginx content.""" blocks = blocks or self.blocks - for key, values in blocks.spaced: + print "iterating", blocks + for b in blocks: + if len(b) == 2: + key, values = b + indentation = "" + elif len(b) == 3: + indentation, key, values = b + assert indentation.isspace(), indentation + " is not space" + yield indentation + else: + print "Cannot process", b + sys.exit(1) #indentation = spacer * current_indent if isinstance(key, list): - yield "".join(key) + ' {' + yield spacer.join(key) + ' {' for parameter in values: dumped = self.__iter__([parameter], current_indent + self.indentation) @@ -75,13 +134,13 @@ class RawNginxDumper(object): yield '}' else: - if key == '#': + if isinstance(key, str) and key.strip() == '#': yield key + values else: if values is None: yield key + ';' else: - yield key + values + ';' + yield key + spacer + values + ';' def __str__(self): """Return the parsed block as a string.""" @@ -138,7 +197,28 @@ def loads(source): :rtype: list """ - return UnspacedList(RawNginxParser(source).as_list()) + old = OldRawNginxParser(source).as_list() + print "Old:" + for entry in old: + print len(entry), " ", + new = UnspacedList(RawNginxParser(source).as_list()) + print "\nNew:" + print new + for entry in new: + print len(entry), " ", + print "\nNewspaced:" + for entry in new.spaced: + print str(len(entry))+ " ", + print "\ngo" + if old != new: + print "NON-MATCH" + for a, b in zip(old, new): + if a != b: + print "Entry", a, "!=", b + import sys + else: + print "Parallel" + return new def load(_file): @@ -149,7 +229,7 @@ def load(_file): :rtype: list """ - return UnspacedList(loads(_file.read())) + return loads(_file.read()) def dumps(blocks, indentation=4): @@ -175,7 +255,6 @@ def dump(blocks, _file, indentation=4): return _file.write(dumps(blocks, indentation)) - spacey = lambda x: isinstance(x, str) and x.isspace() class UnspacedList(list): diff --git a/certbot-nginx/certbot_nginx/parser.py b/certbot-nginx/certbot_nginx/parser.py index 2f08c15d3..2872654b5 100644 --- a/certbot-nginx/certbot_nginx/parser.py +++ b/certbot-nginx/certbot_nginx/parser.py @@ -209,7 +209,7 @@ class NginxParser(object): """ for filename in self.parsed: - tree = self.parsed[filename] + tree = self.parsed[filename].spaced if ext: filename = filename + os.path.extsep + ext try: From e51c16d666655c2c2405ea604c5aee76bf5eb4ca Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Tue, 7 Jun 2016 17:24:56 -0700 Subject: [PATCH 283/396] Update letsencrypt-auto changes for the new format --- letsencrypt-auto-source/letsencrypt-auto | 27 +++++++++++++++++++ .../letsencrypt-auto.template | 4 +++ 2 files changed, 31 insertions(+) diff --git a/letsencrypt-auto-source/letsencrypt-auto b/letsencrypt-auto-source/letsencrypt-auto index 1992c9d47..eef0c957d 100755 --- a/letsencrypt-auto-source/letsencrypt-auto +++ b/letsencrypt-auto-source/letsencrypt-auto @@ -458,12 +458,39 @@ BootstrapSmartOS() { pkgin -y install 'gcc49' 'py27-augeas' 'py27-virtualenv' } +BootstrapMageiaCommon() { + if ! $SUDO urpmi --force \ + python \ + libpython-devel \ + python-virtualenv + then + echo "Could not install Python dependencies. Aborting bootstrap!" + exit 1 + fi + + if ! $SUDO urpmi --force \ + git \ + gcc \ + cdialog \ + python-augeas \ + libopenssl-devel \ + libffi-devel \ + rootcerts + then + echo "Could not install additional dependencies. Aborting bootstrap!" + exit 1 + fi +} + # Install required OS packages: Bootstrap() { if [ -f /etc/debian_version ]; then echo "Bootstrapping dependencies for Debian-based OSes..." BootstrapDebCommon + elif [ -f /etc/mageia-release ] ; then + # Mageia has both /etc/mageia-release and /etc/redhat-release + ExperimentalBootstrap "Mageia" BootstrapMageiaCommon elif [ -f /etc/redhat-release ]; then echo "Bootstrapping dependencies for RedHat-based OSes..." BootstrapRpmCommon diff --git a/letsencrypt-auto-source/letsencrypt-auto.template b/letsencrypt-auto-source/letsencrypt-auto.template index 43d8bc7e1..4d15d281d 100755 --- a/letsencrypt-auto-source/letsencrypt-auto.template +++ b/letsencrypt-auto-source/letsencrypt-auto.template @@ -155,12 +155,16 @@ DeterminePythonVersion() { {{ bootstrappers/free_bsd.sh }} {{ bootstrappers/mac.sh }} {{ bootstrappers/smartos.sh }} +{{ bootstrappers/mageia_common.sh }} # Install required OS packages: Bootstrap() { if [ -f /etc/debian_version ]; then echo "Bootstrapping dependencies for Debian-based OSes..." BootstrapDebCommon + elif [ -f /etc/mageia-release ] ; then + # Mageia has both /etc/mageia-release and /etc/redhat-release + ExperimentalBootstrap "Mageia" BootstrapMageiaCommon elif [ -f /etc/redhat-release ]; then echo "Bootstrapping dependencies for RedHat-based OSes..." BootstrapRpmCommon From 80a52d8f018c99f186d6d7642edc86294852702d Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Tue, 7 Jun 2016 17:55:53 -0700 Subject: [PATCH 284/396] Vaguely close to working? --- certbot-nginx/certbot_nginx/nginxparser.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/certbot-nginx/certbot_nginx/nginxparser.py b/certbot-nginx/certbot_nginx/nginxparser.py index 8c2ba197e..9e7136ced 100644 --- a/certbot-nginx/certbot_nginx/nginxparser.py +++ b/certbot-nginx/certbot_nginx/nginxparser.py @@ -31,7 +31,7 @@ class RawNginxParser(object): # rules comment = Literal('#') + restOfLine() - assignment = keepSpace + key + Optional(space + value, default=None) + semicolon + assignment = keepSpace + key + Optional(White() + value, default=None) + semicolon location_statement = Optional(space + modifier) + Optional(space + location) if_statement = Literal("if") + space + Regex(r"\(.+\)") + space map_statement = Literal("map") + space + Regex(r"\S+") + space + Regex(r"\$\S+") + space @@ -119,14 +119,13 @@ class RawNginxDumper(object): elif len(b) == 3: indentation, key, values = b assert indentation.isspace(), indentation + " is not space" - yield indentation + indentation = indentation.replace("\n", "", 1) else: print "Cannot process", b sys.exit(1) #indentation = spacer * current_indent if isinstance(key, list): - yield spacer.join(key) + ' {' - + yield indentation + spacer.join(key) + ' {' for parameter in values: dumped = self.__iter__([parameter], current_indent + self.indentation) for line in dumped: @@ -135,15 +134,16 @@ class RawNginxDumper(object): yield '}' else: if isinstance(key, str) and key.strip() == '#': - yield key + values + yield indentation + key + values else: if values is None: - yield key + ';' + yield indentation + key + ';' else: - yield key + spacer + values + ';' + yield indentation + key + spacer + values + ';' def __str__(self): """Return the parsed block as a string.""" + print "Merging:\n", '\n'.join(self) return '\n'.join(self) + '\n' From 443f2ebb580faf6a582efe359405c4180f7ee0a6 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Tue, 7 Jun 2016 18:19:58 -0700 Subject: [PATCH 285/396] WIP --- certbot-nginx/certbot_nginx/nginxparser.py | 23 ++++++++++------------ 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/certbot-nginx/certbot_nginx/nginxparser.py b/certbot-nginx/certbot_nginx/nginxparser.py index 9e7136ced..2a68de77b 100644 --- a/certbot-nginx/certbot_nginx/nginxparser.py +++ b/certbot-nginx/certbot_nginx/nginxparser.py @@ -31,7 +31,7 @@ class RawNginxParser(object): # rules comment = Literal('#') + restOfLine() - assignment = keepSpace + key + Optional(White() + value, default=None) + semicolon + assignment = keepSpace + key + Optional(space + value, default=None) + semicolon location_statement = Optional(space + modifier) + Optional(space + location) if_statement = Literal("if") + space + Regex(r"\(.+\)") + space map_statement = Literal("map") + space + Regex(r"\S+") + space + Regex(r"\$\S+") + space @@ -108,31 +108,28 @@ class RawNginxDumper(object): self.blocks = blocks self.indentation = indentation - def __iter__(self, blocks=None, current_indent=0, spacer=' '): + def __iter__(self, blocks=None, spacer=' '): """Iterates the dumped nginx content.""" blocks = blocks or self.blocks print "iterating", blocks for b in blocks: - if len(b) == 2: - key, values = b - indentation = "" - elif len(b) == 3: - indentation, key, values = b - assert indentation.isspace(), indentation + " is not space" + indentation = "" + if spacey(b[0]): + indentation = b.pop(0) indentation = indentation.replace("\n", "", 1) - else: - print "Cannot process", b - sys.exit(1) - #indentation = spacer * current_indent + key = b.pop(0) + + values = b if isinstance(key, list): yield indentation + spacer.join(key) + ' {' for parameter in values: - dumped = self.__iter__([parameter], current_indent + self.indentation) + dumped = self.__iter__([parameter]) for line in dumped: yield line yield '}' else: + #yield b if isinstance(key, str) and key.strip() == '#': yield indentation + key + values else: From 96dd662e558da73d178dd4fef0f14a5f1dc67975 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 8 Jun 2016 14:35:59 -0700 Subject: [PATCH 286/396] Delint certbot-compatibility-test --- .../certbot_compatibility_test/configurators/common.py | 1 - certbot-compatibility-test/certbot_compatibility_test/util.py | 2 -- 2 files changed, 3 deletions(-) diff --git a/certbot-compatibility-test/certbot_compatibility_test/configurators/common.py b/certbot-compatibility-test/certbot_compatibility_test/configurators/common.py index 4592eca39..03128cc86 100644 --- a/certbot-compatibility-test/certbot_compatibility_test/configurators/common.py +++ b/certbot-compatibility-test/certbot_compatibility_test/configurators/common.py @@ -5,7 +5,6 @@ import shutil import tempfile from certbot import constants -from certbot_compatibility_test import errors from certbot_compatibility_test import util diff --git a/certbot-compatibility-test/certbot_compatibility_test/util.py b/certbot-compatibility-test/certbot_compatibility_test/util.py index 570bf1a9e..af951aa6a 100644 --- a/certbot-compatibility-test/certbot_compatibility_test/util.py +++ b/certbot-compatibility-test/certbot_compatibility_test/util.py @@ -1,11 +1,9 @@ """Utility functions for Certbot plugin tests.""" import argparse import copy -import contextlib import os import re import shutil -import socket import tarfile from acme import jose From a0be028340acf1de55426a48ee4e89994f5a4166 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 8 Jun 2016 16:49:08 -0700 Subject: [PATCH 287/396] Add _get_names_from_cert_or_req --- certbot/crypto_util.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/certbot/crypto_util.py b/certbot/crypto_util.py index 6b1b8426c..8640fec1e 100644 --- a/certbot/crypto_util.py +++ b/certbot/crypto_util.py @@ -296,6 +296,18 @@ def get_sans_from_csr(csr, typ=OpenSSL.crypto.FILETYPE_PEM): csr, OpenSSL.crypto.load_certificate_request, typ) +def _get_names_from_cert_or_req(cert_or_req, load_func, typ): + loaded_cert_or_req = _load_cert_or_req(cert_or_req, load_func, typ) + subject = loaded_cert_or_req.get_subject().CN + # pylint: disable=protected-access + sans = acme_crypto_util._pyopenssl_cert_or_req_san(loaded_cert_or_req) + + if subject is None: + return sans + else: + return [subject] + [d for d in sans if d != subject] + + def get_names_from_csr(csr, typ=OpenSSL.crypto.FILETYPE_PEM): """Get a list of domains from a CSR, including the CN if it is set. From ac581951b3926c590ce930230a315d31e91ed0b2 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 8 Jun 2016 16:50:34 -0700 Subject: [PATCH 288/396] Have get_names_from_csr use _get_names_from_cert_or_req --- certbot/crypto_util.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/certbot/crypto_util.py b/certbot/crypto_util.py index 8640fec1e..e8047f086 100644 --- a/certbot/crypto_util.py +++ b/certbot/crypto_util.py @@ -318,13 +318,8 @@ def get_names_from_csr(csr, typ=OpenSSL.crypto.FILETYPE_PEM): :rtype: list """ - loaded_csr = _load_cert_or_req( + return _get_names_from_cert_or_req( csr, OpenSSL.crypto.load_certificate_request, typ) - # Use a set to avoid duplication with CN and Subject Alt Names - domains = set(d for d in (loaded_csr.get_subject().CN,) if d is not None) - # pylint: disable=protected-access - domains.update(acme_crypto_util._pyopenssl_cert_or_req_san(loaded_csr)) - return list(domains) def dump_pyopenssl_chain(chain, filetype=OpenSSL.crypto.FILETYPE_PEM): From 753aea2f3f495eccb3f50cf977c3864dc56a55b6 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 8 Jun 2016 16:53:04 -0700 Subject: [PATCH 289/396] Add get_names_from_cert function --- certbot/crypto_util.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/certbot/crypto_util.py b/certbot/crypto_util.py index e8047f086..f45645de1 100644 --- a/certbot/crypto_util.py +++ b/certbot/crypto_util.py @@ -308,6 +308,20 @@ def _get_names_from_cert_or_req(cert_or_req, load_func, typ): return [subject] + [d for d in sans if d != subject] +def get_names_from_cert(csr, typ=OpenSSL.crypto.FILETYPE_PEM): + """Get a list of domains from a cert, including the CN if it is set. + + :param str cert: Certificate (encoded). + :param typ: `OpenSSL.crypto.FILETYPE_PEM` or `OpenSSL.crypto.FILETYPE_ASN1` + + :returns: A list of domain names. + :rtype: list + + """ + return _get_names_from_cert_or_req( + csr, OpenSSL.crypto.load_certificate, typ) + + def get_names_from_csr(csr, typ=OpenSSL.crypto.FILETYPE_PEM): """Get a list of domains from a CSR, including the CN if it is set. From 8db1b5627c02d4a02890075e1a3ca6f9a3a29d96 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 8 Jun 2016 16:57:56 -0700 Subject: [PATCH 290/396] Add GetNamesFromCertTest --- certbot/tests/crypto_util_test.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/certbot/tests/crypto_util_test.py b/certbot/tests/crypto_util_test.py index fa88e89e7..742d4ec8c 100644 --- a/certbot/tests/crypto_util_test.py +++ b/certbot/tests/crypto_util_test.py @@ -273,6 +273,25 @@ class GetSANsFromCSRTest(unittest.TestCase): [], self._call(test_util.load_vector('csr-nosans.pem'))) +class GetNamesFromCertTest(unittest.TestCase): + """Tests for certbot.crypto_util.get_names_from_cert.""" + + @classmethod + def _call(cls, *args, **kwargs): + from certbot.crypto_util import get_names_from_cert + return get_names_from_cert(*args, **kwargs) + + def test_single(self): + self.assertEqual( + ['example.com'], + self._call(test_util.load_vector('cert.pem'))) + + def test_san(self): + self.assertEqual( + ['example.com', 'www.example.com'], + self._call(test_util.load_vector('cert-san.pem'))) + + class GetNamesFromCSRTest(unittest.TestCase): """Tests for certbot.crypto_util.get_names_from_csr.""" @classmethod From 0a707b64ec0b9a0ee61fe56fc6bcf4b1b9525318 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 8 Jun 2016 16:59:44 -0700 Subject: [PATCH 291/396] Use common_name instead of subject --- certbot/crypto_util.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/certbot/crypto_util.py b/certbot/crypto_util.py index f45645de1..1e831dd8f 100644 --- a/certbot/crypto_util.py +++ b/certbot/crypto_util.py @@ -298,14 +298,14 @@ def get_sans_from_csr(csr, typ=OpenSSL.crypto.FILETYPE_PEM): def _get_names_from_cert_or_req(cert_or_req, load_func, typ): loaded_cert_or_req = _load_cert_or_req(cert_or_req, load_func, typ) - subject = loaded_cert_or_req.get_subject().CN + common_name = loaded_cert_or_req.get_subject().CN # pylint: disable=protected-access sans = acme_crypto_util._pyopenssl_cert_or_req_san(loaded_cert_or_req) - if subject is None: + if common_name is None: return sans else: - return [subject] + [d for d in sans if d != subject] + return [common_name] + [d for d in sans if d != common_name] def get_names_from_cert(csr, typ=OpenSSL.crypto.FILETYPE_PEM): From 2c803eff6ab161079128dca16f8efd8154049ac1 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 8 Jun 2016 17:01:54 -0700 Subject: [PATCH 292/396] Use get_names_from_cert in storage.py --- certbot/storage.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certbot/storage.py b/certbot/storage.py index b0c8245d3..60886e306 100644 --- a/certbot/storage.py +++ b/certbot/storage.py @@ -616,7 +616,7 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes if target is None: raise errors.CertStorageError("could not find cert file") with open(target) as f: - return crypto_util.get_sans_from_cert(f.read()) + return crypto_util.get_names_from_cert(f.read()) def autodeployment_is_enabled(self): """Is automatic deployment enabled for this cert? From 8147c671e42c95cd7aa63f15f2d8bb3cf280c0bd Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Wed, 8 Jun 2016 17:52:35 -0700 Subject: [PATCH 293/396] Now handles some conf files in whitespace-preserving mode (but not all of them) --- certbot-nginx/certbot_nginx/nginxparser.py | 136 ++++----------------- certbot-nginx/certbot_nginx/parser.py | 22 +++- certbot-nginx/certbot_nginx/tls_sni_01.py | 9 +- 3 files changed, 53 insertions(+), 114 deletions(-) diff --git a/certbot-nginx/certbot_nginx/nginxparser.py b/certbot-nginx/certbot_nginx/nginxparser.py index 2a68de77b..13498bee2 100644 --- a/certbot-nginx/certbot_nginx/nginxparser.py +++ b/certbot-nginx/certbot_nginx/nginxparser.py @@ -18,8 +18,7 @@ class RawNginxParser(object): left_bracket = Literal("{").suppress() right_bracket = Literal("}").suppress() semicolon = Literal(";").suppress() - space = White().suppress() - keepSpace = Optional(White()) + space = Optional(White()) key = Word(alphanums + "_/+-.") # Matches anything that is not a special character AND any chars in single # or double quotes @@ -29,21 +28,23 @@ class RawNginxParser(object): modifier = Literal("=") | Literal("~*") | Literal("~") | Literal("^~") # rules - comment = Literal('#') + restOfLine() + comment = space + Literal('#') + restOfLine() - assignment = keepSpace + key + Optional(space + value, default=None) + semicolon - location_statement = Optional(space + modifier) + Optional(space + location) - if_statement = Literal("if") + space + Regex(r"\(.+\)") + space - map_statement = Literal("map") + space + Regex(r"\S+") + space + Regex(r"\$\S+") + space + assignment = space + key + Optional(space + value, default=None) + semicolon + location_statement = space + Optional(modifier) + Optional(space + location) + if_statement = space + Literal("if") + space + Regex(r"\(.+\)") + space + map_statement = space + Literal("map") + space + Regex(r"\S+") + space + Regex(r"\$\S+") + space block = Forward() block << Group( + # XXX could this "key" be Literal("location")? (Group(key + location_statement) ^ Group(if_statement) ^ Group(map_statement)) + left_bracket + Group(ZeroOrMore(Group(comment | assignment) | block)) + right_bracket) script = OneOrMore(Group(comment | assignment) ^ block) + stringEnd + script.parseWithTabs() def __init__(self, source): self.source = source @@ -56,51 +57,6 @@ class RawNginxParser(object): """Returns the parsed tree as a list.""" return self.parse().asList() -class OldRawNginxParser(object): - # pylint: disable=expression-not-assigned - """A class that parses nginx configuration with pyparsing.""" - - # constants - left_bracket = Literal("{").suppress() - right_bracket = Literal("}").suppress() - semicolon = Literal(";").suppress() - space = White().suppress() - key = Word(alphanums + "_/+-.") - # Matches anything that is not a special character AND any chars in single - # or double quotes - value = Regex(r"((\".*\")?(\'.*\')?[^\{\};,]?)+") - location = CharsNotIn("{};," + string.whitespace) - # modifier for location uri [ = | ~ | ~* | ^~ ] - modifier = Literal("=") | Literal("~*") | Literal("~") | Literal("^~") - - # rules - comment = Literal("#") + restOfLine() - assignment = (key + Optional(space + value, default=None) + semicolon) - location_statement = Optional(space + modifier) + Optional(space + location) - if_statement = Literal("if") + space + Regex(r"\(.+\)") + space - map_statement = Literal("map") + space + Regex(r"\S+") + space + Regex(r"\$\S+") + space - block = Forward() - - block << Group( - (Group(key + location_statement) ^ Group(if_statement) ^ Group(map_statement)) + - left_bracket + - Group(ZeroOrMore(Group(comment | assignment) | block)) + - right_bracket) - - script = OneOrMore(Group(comment | assignment) ^ block) + stringEnd - - def __init__(self, source): - self.source = source - - def parse(self): - """Returns the parsed tree.""" - return self.script.parseString(self.source) - - def as_list(self): - """Returns the parsed tree as a list.""" - return self.parse().asList() - - class RawNginxDumper(object): # pylint: disable=too-few-public-methods """A class that dumps nginx configuration from the provided tree.""" @@ -108,35 +64,41 @@ class RawNginxDumper(object): self.blocks = blocks self.indentation = indentation - def __iter__(self, blocks=None, spacer=' '): + def __iter__(self, blocks=None): """Iterates the dumped nginx content.""" blocks = blocks or self.blocks print "iterating", blocks for b in blocks: + b0 = b + b = copy.deepcopy(b) indentation = "" if spacey(b[0]): indentation = b.pop(0) indentation = indentation.replace("\n", "", 1) key = b.pop(0) + values = b.pop(0) - values = b if isinstance(key, list): - yield indentation + spacer.join(key) + ' {' + yield indentation + "".join(key) + '{' for parameter in values: dumped = self.__iter__([parameter]) for line in dumped: yield line - yield '}' else: - #yield b if isinstance(key, str) and key.strip() == '#': yield indentation + key + values else: + gap = "" + # Sometimes the parser has stuck some gap whitespace in here; + # if so rotate it into gap + if spacey(values): + gap = values + values = b.pop(0) if values is None: - yield indentation + key + ';' + yield indentation + key + gap + ';' else: - yield indentation + key + spacer + values + ';' + yield indentation + key + gap + values + ';' def __str__(self): """Return the parsed block as a string.""" @@ -146,42 +108,6 @@ class RawNginxDumper(object): -class OldRawNginxDumper(object): - # pylint: disable=too-few-public-methods - """A class that dumps nginx configuration from the provided tree.""" - def __init__(self, blocks, indentation=4): - self.blocks = blocks - self.indentation = indentation - - def __iter__(self, blocks=None, current_indent=0, spacer=' '): - """Iterates the dumped nginx content.""" - blocks = blocks or self.blocks - for key, values in blocks: - indentation = spacer * current_indent - if isinstance(key, list): - if current_indent: - yield '' - yield indentation + spacer.join(key) + ' {' - - for parameter in values: - dumped = self.__iter__([parameter], current_indent + self.indentation) - for line in dumped: - yield line - - yield indentation + '}' - else: - if key == '#': - yield spacer * current_indent + key + values - else: - if values is None: - yield spacer * current_indent + key + ';' - else: - yield spacer * current_indent + key + spacer + values + ';' - - def __str__(self): - """Return the parsed block as a string.""" - return '\n'.join(self) + '\n' - # Shortcut functions to respect Python's serialization interface # (like pyyaml, picker or json) @@ -194,10 +120,6 @@ def loads(source): :rtype: list """ - old = OldRawNginxParser(source).as_list() - print "Old:" - for entry in old: - print len(entry), " ", new = UnspacedList(RawNginxParser(source).as_list()) print "\nNew:" print new @@ -207,14 +129,6 @@ def loads(source): for entry in new.spaced: print str(len(entry))+ " ", print "\ngo" - if old != new: - print "NON-MATCH" - for a, b in zip(old, new): - if a != b: - print "Entry", a, "!=", b - import sys - else: - print "Parallel" return new @@ -229,7 +143,7 @@ def load(_file): return loads(_file.read()) -def dumps(blocks, indentation=4): +def dumps(blocks): """Dump to a string. :param UnspacedList block: The parsed tree @@ -237,10 +151,10 @@ def dumps(blocks, indentation=4): :rtype: str """ - return str(RawNginxDumper(blocks, indentation)) + return str(RawNginxDumper(blocks)) -def dump(blocks, _file, indentation=4): +def dump(blocks, _file): """Dump to a file. :param UnspacedList block: The parsed tree @@ -249,7 +163,7 @@ def dump(blocks, _file, indentation=4): :rtype: NoneType """ - return _file.write(dumps(blocks, indentation)) + return _file.write(dumps(blocks)) spacey = lambda x: isinstance(x, str) and x.isspace() diff --git a/certbot-nginx/certbot_nginx/parser.py b/certbot-nginx/certbot_nginx/parser.py index 2872654b5..642702c9f 100644 --- a/certbot-nginx/certbot_nginx/parser.py +++ b/certbot-nginx/certbot_nginx/parser.py @@ -4,6 +4,7 @@ import logging import os import pyparsing import re +import sys from certbot import errors @@ -213,9 +214,26 @@ class NginxParser(object): if ext: filename = filename + os.path.extsep + ext try: - logger.debug('Dumping to %s:\n%s', filename, nginxparser.dumps(tree)) + out = nginxparser.dumps(tree) + logger.debug('Dumping to %s:\n%s', filename, out) with open(filename, 'w') as _file: - nginxparser.dump(tree, _file) + _file.write(out) + + if "owncloud" in filename: + print "Outputting", filename + print out + a = open("/tmp/nginx/sites-enabled/owncloud.conf").read() + b = open(filename).read() + for linea, lineb in zip(a.split('\n'), b.split('\n')): + if linea != lineb: + print "a", repr(linea) + print "b", repr(lineb) + if a != b: + print "Mismatch!" + if a != out: + print "Double mismatch", len(a), len(out) + else: + print "Match!" except IOError: logger.error("Could not open file for writing: %s", filename) diff --git a/certbot-nginx/certbot_nginx/tls_sni_01.py b/certbot-nginx/certbot_nginx/tls_sni_01.py index e4c5d31a6..efb5d53e6 100644 --- a/certbot-nginx/certbot_nginx/tls_sni_01.py +++ b/certbot-nginx/certbot_nginx/tls_sni_01.py @@ -3,6 +3,7 @@ import itertools import logging import os +import sys from certbot import errors from certbot.plugins import common @@ -123,7 +124,13 @@ class NginxTlsSni01(common.TLSSNI01): True, self.challenge_conf) with open(self.challenge_conf, "w") as new_conf: - nginxparser.dump(config, new_conf) + if "mime" in self.challenge_conf: + print "Weird" + out = nginxparser.dumps(config) + print out + #sys.exit(1) + #nginxparser.dump(config, new_conf) + new_conf.write(out) def _make_server_block(self, achall, addrs): """Creates a server block for a challenge. From afd899886d255f4c15e89ab7940a561083a3d2c1 Mon Sep 17 00:00:00 2001 From: Willem Fibbe Date: Wed, 8 Jun 2016 16:26:56 +0200 Subject: [PATCH 294/396] Prevent bootstrap-issue on Debian systems with virtualenv package On Debian 7 (and probably relative distro's) `aptitude show virtualenv` exits with 0, since it is a virtual package. However, it doesn't have any installation candidates, so filter on this case before trying to install `virtualenv` to prevent installation-errors while bootstrapping. NB, to make this clear: (0)#: apt-cache show virtualenv N: Can't select versions from package 'virtualenv' as it is purely virtual N: No packages found (0)#: echo $? 0 Furthermore, --quiet=0 is necessary, to be able to grep through `apt-cache`'s output via a pipe. More details on http://unix.stackexchange.com/questions/201869/why-isnt-apt-cache-policy-output-piped/202041#202041. --- letsencrypt-auto-source/letsencrypt-auto | 2 +- letsencrypt-auto-source/pieces/bootstrappers/deb_common.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/letsencrypt-auto-source/letsencrypt-auto b/letsencrypt-auto-source/letsencrypt-auto index 1992c9d47..3ed48216d 100755 --- a/letsencrypt-auto-source/letsencrypt-auto +++ b/letsencrypt-auto-source/letsencrypt-auto @@ -172,7 +172,7 @@ BootstrapDebCommon() { # distro version (#346) virtualenv= - if apt-cache show virtualenv > /dev/null 2>&1; then + if apt-cache show virtualenv > /dev/null 2>&1 && ! apt-cache --quiet=0 show virtualenv 2>&1 | grep -q 'No packages found'; then virtualenv="virtualenv" fi diff --git a/letsencrypt-auto-source/pieces/bootstrappers/deb_common.sh b/letsencrypt-auto-source/pieces/bootstrappers/deb_common.sh index bfbcfa31d..8eb7e16ee 100644 --- a/letsencrypt-auto-source/pieces/bootstrappers/deb_common.sh +++ b/letsencrypt-auto-source/pieces/bootstrappers/deb_common.sh @@ -23,7 +23,7 @@ BootstrapDebCommon() { # distro version (#346) virtualenv= - if apt-cache show virtualenv > /dev/null 2>&1; then + if apt-cache show virtualenv > /dev/null 2>&1 && ! apt-cache --quiet=0 show virtualenv 2>&1 | grep -q 'No packages found'; then virtualenv="virtualenv" fi From 02cdb5db0ef2e035a19b86ca0b462a0a38e50694 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Thu, 9 Jun 2016 16:03:32 -0700 Subject: [PATCH 295/396] Add cert with not alphabetically first CN --- certbot/tests/testdata/cert-5sans.pem | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 certbot/tests/testdata/cert-5sans.pem diff --git a/certbot/tests/testdata/cert-5sans.pem b/certbot/tests/testdata/cert-5sans.pem new file mode 100644 index 000000000..5de7cc6cb --- /dev/null +++ b/certbot/tests/testdata/cert-5sans.pem @@ -0,0 +1,16 @@ +-----BEGIN CERTIFICATE----- +MIICkTCCAjugAwIBAgIJAJNbfABWQ8bbMA0GCSqGSIb3DQEBCwUAMHkxCzAJBgNV +BAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJhbmNp +c2NvMScwJQYDVQQKDB5FbGVjdHJvbmljIEZyb250aWVyIEZvdW5kYXRpb24xFDAS +BgNVBAMMC2V4YW1wbGUuY29tMB4XDTE2MDYwOTIzMDEzNloXDTE2MDcwOTIzMDEz +NloweTELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcM +DVNhbiBGcmFuY2lzY28xJzAlBgNVBAoMHkVsZWN0cm9uaWMgRnJvbnRpZXIgRm91 +bmRhdGlvbjEUMBIGA1UEAwwLZXhhbXBsZS5jb20wXDANBgkqhkiG9w0BAQEFAANL +ADBIAkEArHVztFHtH92ucFJD/N/HW9AsdRsUuHUBBBDlHwNlRd3fp580rv2+6QWE +30cWgdmJS86ObRz6lUTor4R0T+3C5QIDAQABo4GlMIGiMB0GA1UdDgQWBBQmz8jt +S9eUsuQlA1gkjwTAdNWXijAfBgNVHSMEGDAWgBQmz8jtS9eUsuQlA1gkjwTAdNWX +ijAMBgNVHRMEBTADAQH/MFIGA1UdEQRLMEmCDWEuZXhhbXBsZS5jb22CDWIuZXhh +bXBsZS5jb22CDWMuZXhhbXBsZS5jb22CDWQuZXhhbXBsZS5jb22CC2V4YW1wbGUu +Y29tMA0GCSqGSIb3DQEBCwUAA0EAVXmZxB+IJdgFvY2InOYeytTD1QmouDZRtj/T +H/HIpSdsfO7qr4d/ZprI2IhLRxp2S4BiU5Qc5HUkeADcpNd06A== +-----END CERTIFICATE----- From 4f99cc7b2a2f1eb4eb1820e75d909551eea2d1c7 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Thu, 9 Jun 2016 17:43:05 -0700 Subject: [PATCH 296/396] Add _write_out_kind method --- certbot/tests/storage_test.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/certbot/tests/storage_test.py b/certbot/tests/storage_test.py index 0c88d3d55..0579c9f1c 100644 --- a/certbot/tests/storage_test.py +++ b/certbot/tests/storage_test.py @@ -84,6 +84,16 @@ class BaseRenewableCertTest(unittest.TestCase): def tearDown(self): shutil.rmtree(self.tempdir) + def _write_out_kind(self, kind, ver, value=None): + link = getattr(self.test_rc, kind) + if os.path.lexists(link): + os.unlink(link) + os.symlink(os.path.join(os.path.pardir, os.path.pardir, "archive", + "example.org", "{0}{1}.pem".format(kind, ver)), + link) + with open(link, "w") as f: + f.write(kind if value is None else value) + def _write_out_ex_kinds(self): for kind in ALL_FOUR: where = getattr(self.test_rc, kind) From 562802bfd0443f14cd928ee41a4ae73b9453ce39 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Thu, 9 Jun 2016 17:44:33 -0700 Subject: [PATCH 297/396] Refactor common symlink writing code --- certbot/tests/storage_test.py | 124 ++++++---------------------------- 1 file changed, 19 insertions(+), 105 deletions(-) diff --git a/certbot/tests/storage_test.py b/certbot/tests/storage_test.py index 0579c9f1c..44b881fd9 100644 --- a/certbot/tests/storage_test.py +++ b/certbot/tests/storage_test.py @@ -96,16 +96,8 @@ class BaseRenewableCertTest(unittest.TestCase): def _write_out_ex_kinds(self): for kind in ALL_FOUR: - where = getattr(self.test_rc, kind) - os.symlink(os.path.join("..", "..", "archive", "example.org", - "{0}12.pem".format(kind)), where) - with open(where, "w") as f: - f.write(kind) - os.unlink(where) - os.symlink(os.path.join("..", "..", "archive", "example.org", - "{0}11.pem".format(kind)), where) - with open(where, "w") as f: - f.write(kind) + self._write_out_kind(kind, 12) + self._write_out_kind(kind, 11) class RenewableCertTests(BaseRenewableCertTest): @@ -214,10 +206,7 @@ class RenewableCertTests(BaseRenewableCertTest): def test_current_target(self): # Relative path logic - os.symlink(os.path.join("..", "..", "archive", "example.org", - "cert17.pem"), self.test_rc.cert) - with open(self.test_rc.cert, "w") as f: - f.write("cert") + self._write_out_kind("cert", 17) self.assertTrue(os.path.samefile(self.test_rc.current_target("cert"), os.path.join(self.tempdir, "archive", "example.org", @@ -235,12 +224,8 @@ class RenewableCertTests(BaseRenewableCertTest): def test_current_version(self): for ver in (1, 5, 10, 20): - os.symlink(os.path.join("..", "..", "archive", "example.org", - "cert{0}.pem".format(ver)), - self.test_rc.cert) - with open(self.test_rc.cert, "w") as f: - f.write("cert") - os.unlink(self.test_rc.cert) + self._write_out_kind("cert", ver) + os.unlink(self.test_rc.cert) os.symlink(os.path.join("..", "..", "archive", "example.org", "cert10.pem"), self.test_rc.cert) self.assertEqual(self.test_rc.current_version("cert"), 10) @@ -251,61 +236,30 @@ class RenewableCertTests(BaseRenewableCertTest): def test_latest_and_next_versions(self): for ver in xrange(1, 6): for kind in ALL_FOUR: - where = getattr(self.test_rc, kind) - if os.path.islink(where): - os.unlink(where) - os.symlink(os.path.join("..", "..", "archive", "example.org", - "{0}{1}.pem".format(kind, ver)), where) - with open(where, "w") as f: - f.write(kind) + self._write_out_kind(kind, ver) self.assertEqual(self.test_rc.latest_common_version(), 5) self.assertEqual(self.test_rc.next_free_version(), 6) # Having one kind of file of a later version doesn't change the # result - os.unlink(self.test_rc.privkey) - os.symlink(os.path.join("..", "..", "archive", "example.org", - "privkey7.pem"), self.test_rc.privkey) - with open(self.test_rc.privkey, "w") as f: - f.write("privkey") + self._write_out_kind("privkey", 7) self.assertEqual(self.test_rc.latest_common_version(), 5) # ... although it does change the next free version self.assertEqual(self.test_rc.next_free_version(), 8) # Nor does having three out of four change the result - os.unlink(self.test_rc.cert) - os.symlink(os.path.join("..", "..", "archive", "example.org", - "cert7.pem"), self.test_rc.cert) - with open(self.test_rc.cert, "w") as f: - f.write("cert") - os.unlink(self.test_rc.fullchain) - os.symlink(os.path.join("..", "..", "archive", "example.org", - "fullchain7.pem"), self.test_rc.fullchain) - with open(self.test_rc.fullchain, "w") as f: - f.write("fullchain") + self._write_out_kind("cert", 7) + self._write_out_kind("fullchain", 7) self.assertEqual(self.test_rc.latest_common_version(), 5) # If we have everything from a much later version, it does change # the result - ver = 17 for kind in ALL_FOUR: - where = getattr(self.test_rc, kind) - if os.path.islink(where): - os.unlink(where) - os.symlink(os.path.join("..", "..", "archive", "example.org", - "{0}{1}.pem".format(kind, ver)), where) - with open(where, "w") as f: - f.write(kind) + self._write_out_kind(kind, 17) self.assertEqual(self.test_rc.latest_common_version(), 17) self.assertEqual(self.test_rc.next_free_version(), 18) def test_update_link_to(self): for ver in xrange(1, 6): for kind in ALL_FOUR: - where = getattr(self.test_rc, kind) - if os.path.islink(where): - os.unlink(where) - os.symlink(os.path.join("..", "..", "archive", "example.org", - "{0}{1}.pem".format(kind, ver)), where) - with open(where, "w") as f: - f.write(kind) + self._write_out_kind(kind, ver) self.assertEqual(ver, self.test_rc.current_version(kind)) # pylint: disable=protected-access self.test_rc._update_link_to("cert", 3) @@ -322,10 +276,7 @@ class RenewableCertTests(BaseRenewableCertTest): "chain3000.pem") def test_version(self): - os.symlink(os.path.join("..", "..", "archive", "example.org", - "cert12.pem"), self.test_rc.cert) - with open(self.test_rc.cert, "w") as f: - f.write("cert") + self._write_out_kind("cert", 12) # TODO: We should probably test that the directory is still the # same, but it's tricky because we can get an absolute # path out when we put a relative path in. @@ -335,13 +286,7 @@ class RenewableCertTests(BaseRenewableCertTest): def test_update_all_links_to_success(self): for ver in xrange(1, 6): for kind in ALL_FOUR: - where = getattr(self.test_rc, kind) - if os.path.islink(where): - os.unlink(where) - os.symlink(os.path.join("..", "..", "archive", "example.org", - "{0}{1}.pem".format(kind, ver)), where) - with open(where, "w") as f: - f.write(kind) + self._write_out_kind(kind, ver) self.assertEqual(ver, self.test_rc.current_version(kind)) self.assertEqual(self.test_rc.latest_common_version(), 5) for ver in xrange(1, 6): @@ -386,13 +331,7 @@ class RenewableCertTests(BaseRenewableCertTest): def test_has_pending_deployment(self): for ver in xrange(1, 6): for kind in ALL_FOUR: - where = getattr(self.test_rc, kind) - if os.path.islink(where): - os.unlink(where) - os.symlink(os.path.join("..", "..", "archive", "example.org", - "{0}{1}.pem".format(kind, ver)), where) - with open(where, "w") as f: - f.write(kind) + self._write_out_kind(kind, ver) self.assertEqual(ver, self.test_rc.current_version(kind)) for ver in xrange(1, 6): self.test_rc.update_all_links_to(ver) @@ -405,21 +344,12 @@ class RenewableCertTests(BaseRenewableCertTest): def test_names(self): # Trying the current version - test_cert = test_util.load_vector("cert-san.pem") - os.symlink(os.path.join("..", "..", "archive", "example.org", - "cert12.pem"), self.test_rc.cert) - with open(self.test_rc.cert, "w") as f: - f.write(test_cert) + self._write_out_kind("cert", 12, test_util.load_vector("cert-san.pem")) self.assertEqual(self.test_rc.names(), ["example.com", "www.example.com"]) # Trying a non-current version - test_cert = test_util.load_vector("cert.pem") - os.unlink(self.test_rc.cert) - os.symlink(os.path.join("..", "..", "archive", "example.org", - "cert15.pem"), self.test_rc.cert) - with open(self.test_rc.cert, "w") as f: - f.write(test_cert) + self._write_out_kind("cert", 15, test_util.load_vector("cert.pem")) self.assertEqual(self.test_rc.names(12), ["example.com", "www.example.com"]) @@ -490,13 +420,7 @@ class RenewableCertTests(BaseRenewableCertTest): # No pending deployment for ver in xrange(1, 6): for kind in ALL_FOUR: - where = getattr(self.test_rc, kind) - if os.path.islink(where): - os.unlink(where) - os.symlink(os.path.join("..", "..", "archive", "example.org", - "{0}{1}.pem".format(kind, ver)), where) - with open(where, "w") as f: - f.write(kind) + self._write_out_kind(kind, ver) self.assertFalse(self.test_rc.should_autodeploy()) def test_autorenewal_is_enabled(self): @@ -517,11 +441,7 @@ class RenewableCertTests(BaseRenewableCertTest): self.assertFalse(self.test_rc.should_autorenew()) self.test_rc.configuration["autorenew"] = "1" for kind in ALL_FOUR: - where = getattr(self.test_rc, kind) - os.symlink(os.path.join("..", "..", "archive", "example.org", - "{0}12.pem".format(kind)), where) - with open(where, "w") as f: - f.write(kind) + self._write_out_kind(kind, 12) # Mandatory renewal on the basis of OCSP revocation mock_ocsp.return_value = True self.assertTrue(self.test_rc.should_autorenew()) @@ -535,13 +455,7 @@ class RenewableCertTests(BaseRenewableCertTest): for ver in xrange(1, 6): for kind in ALL_FOUR: - where = getattr(self.test_rc, kind) - if os.path.islink(where): - os.unlink(where) - os.symlink(os.path.join("..", "..", "archive", "example.org", - "{0}{1}.pem".format(kind, ver)), where) - with open(where, "w") as f: - f.write(kind) + self._write_out_kind(kind, ver) self.test_rc.update_all_links_to(3) self.assertEqual( 6, self.test_rc.save_successor(3, "new cert", None, From bb1d2c0a1faf7d58249e73fdbd6ea832335755b3 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Thu, 9 Jun 2016 17:49:44 -0700 Subject: [PATCH 298/396] Test common name is listed first in storage.py --- certbot/tests/storage_test.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/certbot/tests/storage_test.py b/certbot/tests/storage_test.py index 44b881fd9..0d907eca3 100644 --- a/certbot/tests/storage_test.py +++ b/certbot/tests/storage_test.py @@ -353,6 +353,13 @@ class RenewableCertTests(BaseRenewableCertTest): self.assertEqual(self.test_rc.names(12), ["example.com", "www.example.com"]) + # Testing common name is listed first + self._write_out_kind( + "cert", 12, test_util.load_vector("cert-5sans.pem")) + self.assertEqual( + self.test_rc.names(12), + ["example.com"] + ["{0}.example.com".format(c) for c in "abcd"]) + # Trying missing cert os.unlink(self.test_rc.cert) self.assertRaises(errors.CertStorageError, self.test_rc.names) From 07cf34284eabf129ce0fae76d79939d38a8bbcdb Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Thu, 9 Jun 2016 17:55:46 -0700 Subject: [PATCH 299/396] Add test_common_name_sans_order --- certbot/tests/crypto_util_test.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/certbot/tests/crypto_util_test.py b/certbot/tests/crypto_util_test.py index 742d4ec8c..5a592bbb1 100644 --- a/certbot/tests/crypto_util_test.py +++ b/certbot/tests/crypto_util_test.py @@ -291,6 +291,13 @@ class GetNamesFromCertTest(unittest.TestCase): ['example.com', 'www.example.com'], self._call(test_util.load_vector('cert-san.pem'))) + def test_common_name_sans_order(self): + # Tests that the common name comes first + # followed by the SANS in alphabetical order + self.assertEqual( + ['example.com'] + ['{0}.example.com'.format(c) for c in 'abcd'], + self._call(test_util.load_vector('cert-5sans.pem'))) + class GetNamesFromCSRTest(unittest.TestCase): """Tests for certbot.crypto_util.get_names_from_csr.""" From bc50ad48c6af5ff1b392dd1379a1cda6fe2d7c04 Mon Sep 17 00:00:00 2001 From: Amjad Mashaal Date: Fri, 10 Jun 2016 06:02:50 +0200 Subject: [PATCH 300/396] Changing default logging level --- certbot/constants.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certbot/constants.py b/certbot/constants.py index 1d4efe80e..fb278161d 100644 --- a/certbot/constants.py +++ b/certbot/constants.py @@ -18,7 +18,7 @@ CLI_DEFAULTS = dict( os.path.join(os.environ.get("XDG_CONFIG_HOME", "~/.config"), "letsencrypt", "cli.ini"), ], - verbose_count=-(logging.WARNING / 10), + verbose_count=-(logging.INFO / 10), server="https://acme-v01.api.letsencrypt.org/directory", rsa_key_size=2048, rollback_checkpoints=1, From b51280875081c868698a2a4b8603c1a9f9905926 Mon Sep 17 00:00:00 2001 From: Amjad Mashaal Date: Fri, 10 Jun 2016 06:15:03 +0200 Subject: [PATCH 301/396] Changing CLI logging format --- certbot/main.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/certbot/main.py b/certbot/main.py index fa14bbf99..bb65680af 100644 --- a/certbot/main.py +++ b/certbot/main.py @@ -615,11 +615,12 @@ def _cli_log_handler(config, level, fmt): def setup_logging(config, cli_handler_factory, logfile): """Setup logging.""" - fmt = "%(asctime)s:%(levelname)s:%(name)s:%(message)s" + file_fmt = "%(asctime)s:%(levelname)s:%(name)s:%(message)s" + cli_fmt = "%(message)s" level = -config.verbose_count * 10 file_handler, log_file_path = setup_log_file_handler( - config, logfile=logfile, fmt=fmt) - cli_handler = cli_handler_factory(config, level, fmt) + config, logfile=logfile, fmt=file_fmt) + cli_handler = cli_handler_factory(config, level, cli_fmt) # TODO: use fileConfig? From 7f9a8f0f089a21769aa80bfd6696cdb19f7f5fa0 Mon Sep 17 00:00:00 2001 From: Amjad Mashaal Date: Fri, 10 Jun 2016 06:26:33 +0200 Subject: [PATCH 302/396] Adjusting logging level of certain messages --- certbot/client.py | 2 +- certbot/main.py | 2 +- certbot/renewal.py | 6 +++--- certbot/reporter.py | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/certbot/client.py b/certbot/client.py index 1dec6a1a9..81b7ccdc6 100644 --- a/certbot/client.py +++ b/certbot/client.py @@ -282,7 +282,7 @@ class Client(object): "by your operating system package manager") if self.config.dry_run: - logger.info("Dry run: Skipping creating new lineage for %s", + logger.debug("Dry run: Skipping creating new lineage for %s", domains[0]) return None else: diff --git a/certbot/main.py b/certbot/main.py index bb65680af..c19375c32 100644 --- a/certbot/main.py +++ b/certbot/main.py @@ -525,7 +525,7 @@ def _csr_obtain_cert(config, le_client): csr, typ = config.actual_csr certr, chain = le_client.obtain_certificate_from_csr(config.domains, csr, typ) if config.dry_run: - logger.info( + logger.debug( "Dry run: skipping saving certificate to %s", config.cert_path) else: cert_path, _, cert_fullchain = le_client.save_certificate( diff --git a/certbot/renewal.py b/certbot/renewal.py index d04e2d27c..059fd6cd9 100644 --- a/certbot/renewal.py +++ b/certbot/renewal.py @@ -107,7 +107,7 @@ def _restore_webroot_config(config, renewalparams): if not cli.set_by_cli("webroot_map"): config.namespace.webroot_map = renewalparams["webroot_map"] elif "webroot_path" in renewalparams: - logger.info("Ancient renewal conf file without webroot-map, restoring webroot-path") + logger.debug("Ancient renewal conf file without webroot-map, restoring webroot-path") wp = renewalparams["webroot_path"] if isinstance(wp, str): # prior to 0.1.0, webroot_path was a string wp = [wp] @@ -193,7 +193,7 @@ def _restore_required_config_elements(config, renewalparams): def should_renew(config, lineage): "Return true if any of the circumstances for automatic renewal apply." if config.renew_by_default: - logger.info("Auto-renewal forced with --force-renewal...") + logger.debug("Auto-renewal forced with --force-renewal...") return True if lineage.should_autorenew(interactive=True): logger.info("Cert is due for renewal, auto-renewing...") @@ -235,7 +235,7 @@ def renew_cert(config, domains, le_client, lineage): _avoid_invalidating_lineage(config, lineage, original_server) new_certr, new_chain, new_key, _ = le_client.obtain_certificate(domains) if config.dry_run: - logger.info("Dry run: skipping updating lineage at %s", + logger.debug("Dry run: skipping updating lineage at %s", os.path.dirname(lineage.cert)) else: prior_version = lineage.latest_common_version() diff --git a/certbot/reporter.py b/certbot/reporter.py index e07011aee..118b13166 100644 --- a/certbot/reporter.py +++ b/certbot/reporter.py @@ -58,7 +58,7 @@ class Reporter(object): """ assert self.HIGH_PRIORITY <= priority <= self.LOW_PRIORITY self.messages.put(self._msg_type(priority, msg, on_crash)) - logger.info("Reporting to user: %s", msg) + logger.debug("Reporting to user: %s", msg) def atexit_print_messages(self, pid=None): """Function to be registered with atexit to print messages. From 6a53522a6cbda3819bba0fd93072c50ef8095869 Mon Sep 17 00:00:00 2001 From: Sergey Nuzdhin Date: Mon, 13 Jun 2016 14:43:47 +0200 Subject: [PATCH 303/396] Add additional warning with actual exception message during renewal Log and show warning with real exception message to make it more clear what exactly happened. Currently we show `config is broken` when in fact we have broken symlinks in live folder. --- certbot/renewal.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/certbot/renewal.py b/certbot/renewal.py index d04e2d27c..95f64b94d 100644 --- a/certbot/renewal.py +++ b/certbot/renewal.py @@ -60,7 +60,8 @@ def _reconstitute(config, full_path): try: renewal_candidate = storage.RenewableCert( full_path, configuration.RenewerConfiguration(config)) - except (errors.CertStorageError, IOError): + except (errors.CertStorageError, IOError) as exc: + logger.warning(exc) logger.warning("Renewal configuration file %s is broken. Skipping.", full_path) logger.debug("Traceback was:\n%s", traceback.format_exc()) return None From f8a07a8f4673d0771c6dae034bb755b10ddb8c15 Mon Sep 17 00:00:00 2001 From: Jacob Hoffman-Andrews Date: Mon, 13 Jun 2016 11:53:32 -0700 Subject: [PATCH 304/396] Provide nonroot guidance when logging gets EACCES. --- certbot/main.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/certbot/main.py b/certbot/main.py index fa14bbf99..f68373998 100644 --- a/certbot/main.py +++ b/certbot/main.py @@ -1,6 +1,7 @@ """Certbot main entry point.""" from __future__ import print_function import atexit +import errno import functools import logging.handlers import os @@ -588,8 +589,16 @@ def renew(config, unused_plugins): def setup_log_file_handler(config, logfile, fmt): """Setup file debug logging.""" log_file_path = os.path.join(config.logs_dir, logfile) - handler = logging.handlers.RotatingFileHandler( - log_file_path, maxBytes=2 ** 20, backupCount=10) + try: + handler = logging.handlers.RotatingFileHandler( + log_file_path, maxBytes=2 ** 20, backupCount=10) + except IOError as e: + if e.errno == errno.EACCES: + msg = ("Access denied writing to {0}. To run as non-root, set " + + "--logs-dir, --config-dir, --work-dir to writable paths.") + raise errors.Error(msg.format(log_file_path)) + else: + raise # rotate on each invocation, rollover only possible when maxBytes # is nonzero and backupCount is nonzero, so we set maxBytes as big # as possible not to overrun in single CLI invocation (1MB). From c60c28514c6a8dde2f94c5d557cce7649b67a344 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 13 Jun 2016 13:49:32 -0700 Subject: [PATCH 305/396] Add global DEFAULT variable --- certbot/cli.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/certbot/cli.py b/certbot/cli.py index cff111f42..e2475ec31 100644 --- a/certbot/cli.py +++ b/certbot/cli.py @@ -104,6 +104,11 @@ ZERO_ARG_ACTIONS = set(("store_const", "store_true", "store_false", "append_const", "count",)) +# Maps a config option to its default value. This is set during the +# parse_args method of HelpfulArgumentParser. +DEFAULTS = None + + # Maps a config option to a set of config options that may have modified it. # This dictionary is used recursively, so if A modifies B and B modifies C, # it is determined that C was modified by the user if A was modified. From 657c4e725992abba9e2c80a9cd7598d97b79b910 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 13 Jun 2016 14:49:24 -0700 Subject: [PATCH 306/396] Set DEFAULTS during parse_args --- certbot/cli.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/certbot/cli.py b/certbot/cli.py index e2475ec31..3c65527e1 100644 --- a/certbot/cli.py +++ b/certbot/cli.py @@ -1,6 +1,7 @@ """Certbot command line argument & config processing.""" from __future__ import print_function import argparse +import copy import glob import logging import logging.handlers @@ -340,6 +341,10 @@ class HelpfulArgumentParser(object): if self.detect_defaults: return parsed_args + global DEFAULTS # pylint: disable=global-statement + DEFAULTS = dict((key, copy.deepcopy(self.parser.get_default(key))) + for key in vars(parsed_args)) + # Do any post-parsing homework here if self.verb == "renew" and not parsed_args.dialog_mode: From b57677b16a994d21659ea2eed7557c9a583712d1 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 13 Jun 2016 14:57:14 -0700 Subject: [PATCH 307/396] Use cli.DEFAULTS in storage.py --- certbot/storage.py | 24 +++++------------------- 1 file changed, 5 insertions(+), 19 deletions(-) diff --git a/certbot/storage.py b/certbot/storage.py index 60886e306..7602ece2d 100644 --- a/certbot/storage.py +++ b/certbot/storage.py @@ -162,30 +162,16 @@ def relevant_values(all_values): from certbot import cli - def _is_cli_default(option, value): - # Look through the CLI parser defaults and see if this option is - # both present and equal to the specified value. If not, return - # False. - # pylint: disable=protected-access - for x in cli.helpful_parser.parser._actions: - if x.dest == option: - if x.default == value: - return True - else: - break - return False - values = dict() for option, value in all_values.iteritems(): # Try to find reasons to store this item in the # renewal config. It can be stored if it is relevant and - # (it is set_by_cli() or flag_default() is different - # from the value or flag_default() doesn't exist). + # (it is set_by_cli(), we don't know the default value, or + # the current value differs from the default value). if _relevant(option): - if (cli.set_by_cli(option) - or not _is_cli_default(option, value)): -# or option not in constants.CLI_DEFAULTS -# or constants.CLI_DEFAULTS[option] != value): + if (cli.set_by_cli(option) or + option not in cli.DEFAULTS or + cli.DEFAULTS[option] != value): values[option] = value return values From 26316fb222cb3cf180810bbf8cdea85e8ddeaf95 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 13 Jun 2016 15:06:49 -0700 Subject: [PATCH 308/396] Ensure changes to webroot_map aren't reflected in cli.DEFAULTS --- certbot/tests/cli_test.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/certbot/tests/cli_test.py b/certbot/tests/cli_test.py index f9557abfb..03edaa18f 100644 --- a/certbot/tests/cli_test.py +++ b/certbot/tests/cli_test.py @@ -490,6 +490,12 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods conflicts += ['--staging'] self._check_server_conflict_message(short_args, conflicts) + def test_defaults_global(self): + namespace = self._get_argument_parser()([]) + namespace.webroot_map['example.com'] = '/var/www/html' + + self.assertTrue(cli.DEFAULTS != namespace.webroot_map) + def _certonly_new_request_common(self, mock_client, args=None): with mock.patch('certbot.main._treat_as_renewal') as mock_renewal: mock_renewal.return_value = ("newcert", None) From 8f6866309781f656725c0ef9d6e89a7b291f4aed Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 13 Jun 2016 16:52:35 -0700 Subject: [PATCH 309/396] Add option_was_set function --- certbot/cli.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/certbot/cli.py b/certbot/cli.py index 3c65527e1..428227657 100644 --- a/certbot/cli.py +++ b/certbot/cli.py @@ -217,6 +217,21 @@ def set_by_cli(var): set_by_cli.detector = None +def option_was_set(option, value): + """Was option set by the user or does it differ from the default? + + :param str option: configuration variable being considered + :param value: value of the configuration variable named option + + :returns: True if the option was set, otherwise, False + :rtype: bool + + """ + return (set_by_cli(option) or + option not in DEFAULTS or + DEFAULTS[option] != value) + + def argparse_type(variable): "Return our argparse type function for a config variable (default: str)" # pylint: disable=protected-access From aaf93b65b0b1b08bec02e84ea92145d3f15e7081 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 13 Jun 2016 16:55:45 -0700 Subject: [PATCH 310/396] Refactor storage.relevant_values --- certbot/storage.py | 23 ++++++++--------------- 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/certbot/storage.py b/certbot/storage.py index 7602ece2d..82fdbfd54 100644 --- a/certbot/storage.py +++ b/certbot/storage.py @@ -7,8 +7,10 @@ import re import configobj import parsedatetime import pytz +import six import certbot +from certbot import cli from certbot import constants from certbot import crypto_util from certbot import errors @@ -158,22 +160,13 @@ def relevant_values(all_values): :param dict all_values: The original values. :returns: A new dictionary containing items that can be used in renewal. - :rtype dict:""" + :rtype dict: - from certbot import cli - - values = dict() - for option, value in all_values.iteritems(): - # Try to find reasons to store this item in the - # renewal config. It can be stored if it is relevant and - # (it is set_by_cli(), we don't know the default value, or - # the current value differs from the default value). - if _relevant(option): - if (cli.set_by_cli(option) or - option not in cli.DEFAULTS or - cli.DEFAULTS[option] != value): - values[option] = value - return values + """ + return dict( + (option, value) + for option, value in six.iteritems(all_values) + if _relevant(option) and cli.option_was_set(option, value)) class RenewableCert(object): # pylint: disable=too-many-instance-attributes From 8c174f38b5e59ecf2298abf08a84c52110cc4091 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 13 Jun 2016 17:00:59 -0700 Subject: [PATCH 311/396] Add has_default_value method --- certbot/cli.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/certbot/cli.py b/certbot/cli.py index 428227657..3c18c3c98 100644 --- a/certbot/cli.py +++ b/certbot/cli.py @@ -217,6 +217,21 @@ def set_by_cli(var): set_by_cli.detector = None +def has_default_value(option, value): + """Does option have the default value? + + If the default value of option is not known, False is returned. + + :param str option: configuration variable being considered + :param value: value of the configuration variable named option + + :returns: True if option has the default value, otherwise, False + :rtype: bool + + """ + return option in DEFAULTS and DEFAULTS[option] == value + + def option_was_set(option, value): """Was option set by the user or does it differ from the default? @@ -227,9 +242,7 @@ def option_was_set(option, value): :rtype: bool """ - return (set_by_cli(option) or - option not in DEFAULTS or - DEFAULTS[option] != value) + return set_by_cli(option) or not has_default_value(option, value) def argparse_type(variable): From 4c68792dd37cc93e17038eba7cd8bee94cdcc6c1 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 13 Jun 2016 17:29:56 -0700 Subject: [PATCH 312/396] Remove test_defaults_global --- certbot/tests/cli_test.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/certbot/tests/cli_test.py b/certbot/tests/cli_test.py index 03edaa18f..f9557abfb 100644 --- a/certbot/tests/cli_test.py +++ b/certbot/tests/cli_test.py @@ -490,12 +490,6 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods conflicts += ['--staging'] self._check_server_conflict_message(short_args, conflicts) - def test_defaults_global(self): - namespace = self._get_argument_parser()([]) - namespace.webroot_map['example.com'] = '/var/www/html' - - self.assertTrue(cli.DEFAULTS != namespace.webroot_map) - def _certonly_new_request_common(self, mock_client, args=None): with mock.patch('certbot.main._treat_as_renewal') as mock_renewal: mock_renewal.return_value = ("newcert", None) From 97af8dfb701edece39ae429852ac1a9784f991e2 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 13 Jun 2016 17:31:57 -0700 Subject: [PATCH 313/396] Add defaults to helpful_parser --- certbot/cli.py | 14 +++++--------- certbot/tests/storage_test.py | 5 ++--- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/certbot/cli.py b/certbot/cli.py index 3c18c3c98..ba1f23708 100644 --- a/certbot/cli.py +++ b/certbot/cli.py @@ -105,11 +105,6 @@ ZERO_ARG_ACTIONS = set(("store_const", "store_true", "store_false", "append_const", "count",)) -# Maps a config option to its default value. This is set during the -# parse_args method of HelpfulArgumentParser. -DEFAULTS = None - - # Maps a config option to a set of config options that may have modified it. # This dictionary is used recursively, so if A modifies B and B modifies C, # it is determined that C was modified by the user if A was modified. @@ -229,7 +224,8 @@ def has_default_value(option, value): :rtype: bool """ - return option in DEFAULTS and DEFAULTS[option] == value + return (option in helpful_parser.defaults and + helpful_parser.defaults[option] == value) def option_was_set(option, value): @@ -354,6 +350,7 @@ class HelpfulArgumentParser(object): sys.exit(0) self.visible_topics = self.determine_help_topics(self.help_arg) self.groups = {} # elements are added by .add_group() + self.defaults = {} # elements are added by .parse_args() def parse_args(self): """Parses command line arguments and returns the result. @@ -369,9 +366,8 @@ class HelpfulArgumentParser(object): if self.detect_defaults: return parsed_args - global DEFAULTS # pylint: disable=global-statement - DEFAULTS = dict((key, copy.deepcopy(self.parser.get_default(key))) - for key in vars(parsed_args)) + self.defaults = dict((key, copy.deepcopy(self.parser.get_default(key))) + for key in vars(parsed_args)) # Do any post-parsing homework here diff --git a/certbot/tests/storage_test.py b/certbot/tests/storage_test.py index 0d907eca3..138f6e2fa 100644 --- a/certbot/tests/storage_test.py +++ b/certbot/tests/storage_test.py @@ -533,10 +533,9 @@ class RenewableCertTests(BaseRenewableCertTest): """Test that relevant_values() can reject a default value.""" # pylint: disable=protected-access from certbot import storage - mock_parser.verb = "certonly" mock_parser.args = ["--standalone"] - mock_action = mock.Mock(dest="rsa_key_size", default=2048) - mock_parser.parser._actions = [mock_action] + mock_parser.defaults = {"rsa_key_size": 2048} + mock_parser.verb = "certonly" self.assertEqual(storage.relevant_values({"rsa_key_size": 2048}), {}) @mock.patch("certbot.cli.helpful_parser") From f9d5ecaf6fc6776988ddf5ea9dbb010ad45eeb7f Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 13 Jun 2016 17:45:47 -0700 Subject: [PATCH 314/396] Add option_was_set test --- certbot/tests/cli_test.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/certbot/tests/cli_test.py b/certbot/tests/cli_test.py index f9557abfb..9d6335b40 100644 --- a/certbot/tests/cli_test.py +++ b/certbot/tests/cli_test.py @@ -447,6 +447,19 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods short_args += '--server example.com'.split() self._check_server_conflict_message(short_args, '--staging') + def test_option_was_set(self): + key_size_option = 'rsa_key_size' + key_size_value = cli.flag_default(key_size_option) + self._get_argument_parser()( + '--rsa-key-size {0}'.format(key_size_value).split()) + + self.assertTrue(cli.option_was_set(key_size_option, key_size_value)) + self.assertTrue(cli.option_was_set('no_verify_ssl', True)) + + config_dir_option = 'config_dir' + self.assertFalse(cli.option_was_set( + config_dir_option, cli.flag_default(config_dir_option))) + def _assert_dry_run_flag_worked(self, namespace, existing_account): self.assertTrue(namespace.dry_run) self.assertTrue(namespace.break_my_certs) From 8c8125c6fd120e7e8d5297ef43ba449b70e924ac Mon Sep 17 00:00:00 2001 From: Geoffroy Doucet Date: Fri, 10 Jun 2016 20:33:25 -0400 Subject: [PATCH 315/396] Added the argument --quiet and -q so then when used with a regular user there is no output to the screen. --- letsencrypt-auto-source/letsencrypt-auto | 13 ++++++++++--- letsencrypt-auto-source/letsencrypt-auto.template | 13 ++++++++++--- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/letsencrypt-auto-source/letsencrypt-auto b/letsencrypt-auto-source/letsencrypt-auto index 569f6d6f3..0ef97c8c1 100755 --- a/letsencrypt-auto-source/letsencrypt-auto +++ b/letsencrypt-auto-source/letsencrypt-auto @@ -34,6 +34,7 @@ Help for certbot itself cannot be provided until it is installed. -n, --non-interactive, --noninteractive run without asking for user input --no-self-upgrade do not download updates --os-packages-only install OS dependencies and exit + -q, --quiet provide only update/error output -v, --verbose provide more output All arguments are accepted and forwarded to the Certbot client when run." @@ -52,15 +53,19 @@ for arg in "$@" ; do HELP=1;; --noninteractive|--non-interactive) ASSUME_YES=1;; + --quiet) + QUIET=1;; --verbose) VERBOSE=1;; -[!-]*) - while getopts ":hnv" short_arg $arg; do + while getopts ":hnvq" short_arg $arg; do case "$short_arg" in h) HELP=1;; n) ASSUME_YES=1;; + q) + QUIET=1;; v) VERBOSE=1;; esac @@ -898,8 +903,10 @@ UNLIKELY_EOF fi if [ -n "$SUDO" ]; then # SUDO is su wrapper or sudo - echo "Requesting root privileges to run certbot..." - echo " $VENV_BIN/letsencrypt" "$@" + if [ "$QUIET" != 1 ]; then + echo "Requesting root privileges to run certbot..." + echo " $VENV_BIN/letsencrypt" "$@" + fi fi if [ -z "$SUDO_ENV" ] ; then # SUDO is su wrapper / noop diff --git a/letsencrypt-auto-source/letsencrypt-auto.template b/letsencrypt-auto-source/letsencrypt-auto.template index 73d819b4a..b254cd429 100755 --- a/letsencrypt-auto-source/letsencrypt-auto.template +++ b/letsencrypt-auto-source/letsencrypt-auto.template @@ -34,6 +34,7 @@ Help for certbot itself cannot be provided until it is installed. -n, --non-interactive, --noninteractive run without asking for user input --no-self-upgrade do not download updates --os-packages-only install OS dependencies and exit + -q, --quiet provide only update/error output -v, --verbose provide more output All arguments are accepted and forwarded to the Certbot client when run." @@ -52,15 +53,19 @@ for arg in "$@" ; do HELP=1;; --noninteractive|--non-interactive) ASSUME_YES=1;; + --quiet) + QUIET=1;; --verbose) VERBOSE=1;; -[!-]*) - while getopts ":hnv" short_arg $arg; do + while getopts ":hnvq" short_arg $arg; do case "$short_arg" in h) HELP=1;; n) ASSUME_YES=1;; + q) + QUIET=1;; v) VERBOSE=1;; esac @@ -257,8 +262,10 @@ UNLIKELY_EOF fi if [ -n "$SUDO" ]; then # SUDO is su wrapper or sudo - echo "Requesting root privileges to run certbot..." - echo " $VENV_BIN/letsencrypt" "$@" + if [ "$QUIET" != 1 ]; then + echo "Requesting root privileges to run certbot..." + echo " $VENV_BIN/letsencrypt" "$@" + fi fi if [ -z "$SUDO_ENV" ] ; then # SUDO is su wrapper / noop From 61b77766c26233745b922ef8777e6694c49ba053 Mon Sep 17 00:00:00 2001 From: Ben Irving Date: Tue, 14 Jun 2016 11:28:29 -0700 Subject: [PATCH 316/396] Add integration test cases for must staple and ECDSA (#3158) --- tests/boulder-integration.sh | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/boulder-integration.sh b/tests/boulder-integration.sh index 323ea004b..ab8fde5f6 100755 --- a/tests/boulder-integration.sh +++ b/tests/boulder-integration.sh @@ -84,6 +84,24 @@ if [ "$size1" -lt 3000 ] || [ "$size2" -lt 3000 ] || [ "$size3" -gt 1800 ] ; the exit 1 fi +# ECDSA +openssl ecparam -genkey -name secp384r1 -out "${root}/privkey-p384.pem" +SAN="DNS:ecdsa.le.wtf" openssl req -new -sha256 \ + -config "${OPENSSL_CNF:-openssl.cnf}" \ + -key "${root}/privkey-p384.pem" \ + -subj "/" \ + -reqexts san \ + -outform der \ + -out "${root}/csr-p384.der" +common auth --csr "${root}/csr-p384.der" \ + --cert-path "${root}/csr/cert-p384.pem" \ + --chain-path "${root}/csr/chain-p384.pem" +openssl x509 -in "${root}/csr/cert-p384.pem" -text | grep 'ASN1 OID: secp384r1' + +# OCSP Must Staple +common auth --must-staple --domains "must-staple.le.wtf" +openssl x509 -in "${root}/conf/live/must-staple.le.wtf/cert.pem" -text | grep '1.3.6.1.5.5.7.1.24' + # revoke by account key common revoke --cert-path "$root/conf/live/le.wtf/cert.pem" # revoke renewed From 261046a2d7dd01a4ce5383d192ce82d34a92faf8 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Tue, 14 Jun 2016 13:52:39 -0700 Subject: [PATCH 317/396] Update relevant_values tests --- certbot/tests/storage_test.py | 50 ++++++++++++++++------------------- 1 file changed, 23 insertions(+), 27 deletions(-) diff --git a/certbot/tests/storage_test.py b/certbot/tests/storage_test.py index 138f6e2fa..261500b98 100644 --- a/certbot/tests/storage_test.py +++ b/certbot/tests/storage_test.py @@ -11,6 +11,7 @@ import mock import pytz import certbot +from certbot import cli from certbot import configuration from certbot import errors from certbot.storage import ALL_FOUR @@ -517,38 +518,33 @@ class RenewableCertTests(BaseRenewableCertTest): self.assertFalse(os.path.islink(self.test_rc.version("privkey", 10))) self.assertFalse(os.path.exists(temp_config_file)) - @mock.patch("certbot.cli.helpful_parser") - def test_relevant_values(self, mock_parser): + def _test_relevant_values_common(self, values): + option = "rsa_key_size" + mock_parser = mock.Mock(args=["--standalone"], verb="certonly", + defaults={option: cli.flag_default(option)}) + + from certbot.storage import relevant_values + with mock.patch("certbot.cli.helpful_parser", mock_parser): + return relevant_values(values) + + def test_relevant_values(self): """Test that relevant_values() can reject an irrelevant value.""" - # pylint: disable=protected-access - from certbot import storage - mock_parser.verb = "certonly" - mock_parser.args = ["--standalone"] - mock_action = mock.Mock(dest="rsa_key_size", default=2048) - mock_parser.parser._actions = [mock_action] - self.assertEqual(storage.relevant_values({"hello": "there"}), {}) + self.assertEqual( + self._test_relevant_values_common({"hello": "there"}), {}) - @mock.patch("certbot.cli.helpful_parser") - def test_relevant_values_default(self, mock_parser): + def test_relevant_values_default(self): """Test that relevant_values() can reject a default value.""" - # pylint: disable=protected-access - from certbot import storage - mock_parser.args = ["--standalone"] - mock_parser.defaults = {"rsa_key_size": 2048} - mock_parser.verb = "certonly" - self.assertEqual(storage.relevant_values({"rsa_key_size": 2048}), {}) + option = "rsa_key_size" + values = {option: cli.flag_default(option)} + self.assertEqual(self._test_relevant_values_common(values), {}) - @mock.patch("certbot.cli.helpful_parser") - def test_relevant_values_nondefault(self, mock_parser): + def test_relevant_values_nondefault(self): """Test that relevant_values() can retain a non-default value.""" - # pylint: disable=protected-access - from certbot import storage - mock_parser.verb = "certonly" - mock_parser.args = ["--standalone"] - mock_action = mock.Mock(dest="rsa_key_size", default=2048) - mock_parser.parser._actions = [mock_action] - self.assertEqual(storage.relevant_values({"rsa_key_size": 12}), - {"rsa_key_size": 12}) + values = {"rsa_key_size": 12} + # A copy is given to _test_relevant_values_common + # to make sure values isn't modified by the method + self.assertEqual( + self._test_relevant_values_common(values.copy()), values) @mock.patch("certbot.storage.relevant_values") def test_new_lineage(self, mock_rv): From 4158656058cb14e8afa9048b9a3612da73abf1b5 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Tue, 14 Jun 2016 16:56:16 -0700 Subject: [PATCH 318/396] Release 0.8.1 --- acme/setup.py | 2 +- certbot-apache/setup.py | 2 +- certbot-auto | 52 ++++++++++++++---- certbot-compatibility-test/setup.py | 2 +- certbot-nginx/setup.py | 2 +- certbot/__init__.py | 2 +- docs/cli-help.txt | 9 +++ letsencrypt-auto | 52 ++++++++++++++---- letsencrypt-auto-source/certbot-auto.asc | 14 ++--- letsencrypt-auto-source/letsencrypt-auto | 20 +++---- letsencrypt-auto-source/letsencrypt-auto.sig | Bin 256 -> 256 bytes .../pieces/letsencrypt-auto-requirements.txt | 18 +++--- 12 files changed, 120 insertions(+), 55 deletions(-) diff --git a/acme/setup.py b/acme/setup.py index ed133e128..2bcec9128 100644 --- a/acme/setup.py +++ b/acme/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.9.0.dev0' +version = '0.8.1' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot-apache/setup.py b/certbot-apache/setup.py index e3dbe4563..87214d2e5 100644 --- a/certbot-apache/setup.py +++ b/certbot-apache/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.9.0.dev0' +version = '0.8.1' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot-auto b/certbot-auto index 2de5ff48f..80f39cf59 100755 --- a/certbot-auto +++ b/certbot-auto @@ -19,7 +19,7 @@ XDG_DATA_HOME=${XDG_DATA_HOME:-~/.local/share} VENV_NAME="letsencrypt" VENV_PATH=${VENV_PATH:-"$XDG_DATA_HOME/$VENV_NAME"} VENV_BIN="$VENV_PATH/bin" -LE_AUTO_VERSION="0.8.0" +LE_AUTO_VERSION="0.8.1" BASENAME=$(basename $0) USAGE="Usage: $BASENAME [OPTIONS] A self-updating wrapper script for the Certbot ACME client. When run, updates @@ -172,7 +172,7 @@ BootstrapDebCommon() { # distro version (#346) virtualenv= - if apt-cache show virtualenv > /dev/null 2>&1; then + if apt-cache show virtualenv > /dev/null 2>&1 && ! apt-cache --quiet=0 show virtualenv 2>&1 | grep -q 'No packages found'; then virtualenv="virtualenv" fi @@ -458,12 +458,39 @@ BootstrapSmartOS() { pkgin -y install 'gcc49' 'py27-augeas' 'py27-virtualenv' } +BootstrapMageiaCommon() { + if ! $SUDO urpmi --force \ + python \ + libpython-devel \ + python-virtualenv + then + echo "Could not install Python dependencies. Aborting bootstrap!" + exit 1 + fi + + if ! $SUDO urpmi --force \ + git \ + gcc \ + cdialog \ + python-augeas \ + libopenssl-devel \ + libffi-devel \ + rootcerts + then + echo "Could not install additional dependencies. Aborting bootstrap!" + exit 1 + fi +} + # Install required OS packages: Bootstrap() { if [ -f /etc/debian_version ]; then echo "Bootstrapping dependencies for Debian-based OSes..." BootstrapDebCommon + elif [ -f /etc/mageia-release ] ; then + # Mageia has both /etc/mageia-release and /etc/redhat-release + ExperimentalBootstrap "Mageia" BootstrapMageiaCommon elif [ -f /etc/redhat-release ]; then echo "Bootstrapping dependencies for RedHat-based OSes..." BootstrapRpmCommon @@ -476,7 +503,7 @@ Bootstrap() { BootstrapArchCommon else echo "Please use pacman to install letsencrypt packages:" - echo "# pacman -S letsencrypt letsencrypt-apache" + echo "# pacman -S certbot certbot-apache" echo echo "If you would like to use the virtualenv way, please run the script again with the" echo "--debug flag." @@ -500,6 +527,7 @@ Bootstrap() { echo "You will need to bootstrap, configure virtualenv, and run pip install manually." echo "Please see https://letsencrypt.readthedocs.org/en/latest/contributing.html#prerequisites" echo "for more info." + exit 1 fi } @@ -719,15 +747,15 @@ letsencrypt==0.7.0 \ # THE LINES BELOW ARE EDITED BY THE RELEASE SCRIPT; ADD ALL DEPENDENCIES ABOVE. -acme==0.8.0 \ - --hash=sha256:8561d590e496afb41a8ff2dac389199661d9cd785b1636ae08325771511189af \ - --hash=sha256:dfa86b547628b231f275c7e0efc7a09bec5dfaec866f89f5c5b59b78c14564da -certbot==0.8.0 \ - --hash=sha256:395c5840ff6b75aa51ee6449c86d016c14c5f65a71281e7bcef5feecac6a3293 \ - --hash=sha256:3c3c70b484fb3243a166515adc81ae0401c5d687a2763c75b40df9d8241a4314 -certbot-apache==0.8.0 \ - --hash=sha256:f4d4fc962ecc19646f6745d49c62a265d26e5b2df3acf34ef4865351594156e3 \ - --hash=sha256:cfb211debbcb0d0645c88d7e8bb38c591fca263bfdb5337242c023956055e268 +acme==0.8.1 \ + --hash=sha256:ccd7883772efbf933f91713b8241455993834f3620c8fbd459d9ed5e50bbaaca \ + --hash=sha256:d3ea4acf280bf6253ad7d641cb0970f230a19805acfed809e7a8ddcf62157d9f +certbot==0.8.1 \ + --hash=sha256:89805d9f70249ae859ec4d7a99c00b4bb7083ca90cd12d4d202b76dfc284f7c5 \ + --hash=sha256:6ca8df3d310ced6687d38aac17c0fb8c1b2ec7a3bea156a254e4cc2a1c132771 +certbot-apache==0.8.1 \ + --hash=sha256:c9e3fdc15e65589c2e39eb0e6b1f61f0c0a1db3c17b00bb337f0ff636cc61cb3 \ + --hash=sha256:0faf2879884d3b7a58b071902fba37d4b8b58a50e2c3b8ac262c0a74134045ed UNLIKELY_EOF # ------------------------------------------------------------------------- diff --git a/certbot-compatibility-test/setup.py b/certbot-compatibility-test/setup.py index fb56be65f..cd282af50 100644 --- a/certbot-compatibility-test/setup.py +++ b/certbot-compatibility-test/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.9.0.dev0' +version = '0.8.1' install_requires = [ 'certbot', diff --git a/certbot-nginx/setup.py b/certbot-nginx/setup.py index 62c705b4c..f76cbe596 100644 --- a/certbot-nginx/setup.py +++ b/certbot-nginx/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.9.0.dev0' +version = '0.8.1' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot/__init__.py b/certbot/__init__.py index 34358a5d9..e5d5629b1 100644 --- a/certbot/__init__.py +++ b/certbot/__init__.py @@ -1,4 +1,4 @@ """Certbot client.""" # version number like 1.2.3a0, must have at least 2 parts, like 1.2 -__version__ = '0.9.0.dev0' +__version__ = '0.8.1' diff --git a/docs/cli-help.txt b/docs/cli-help.txt index b25326148..e5f1fdcb4 100644 --- a/docs/cli-help.txt +++ b/docs/cli-help.txt @@ -200,6 +200,15 @@ renew: and keys; the shell variable $RENEWED_DOMAINS will contain a space-delimited list of renewed cert domains (default: None) + --disable-hook-validation + Ordinarily the commands specified for --pre-hook + /--post-hook/--renew-hook will be checked for + validity, to see if the programs being run are in the + $PATH, so that mistakes can be caught early, even when + the hooks aren't being run just yet. The validation is + rather simplistic and fails if you use more advanced + shell constructs, so you can use this switch to + disable it. (default: True) certonly: Options for modifying how a cert is obtained diff --git a/letsencrypt-auto b/letsencrypt-auto index 2de5ff48f..80f39cf59 100755 --- a/letsencrypt-auto +++ b/letsencrypt-auto @@ -19,7 +19,7 @@ XDG_DATA_HOME=${XDG_DATA_HOME:-~/.local/share} VENV_NAME="letsencrypt" VENV_PATH=${VENV_PATH:-"$XDG_DATA_HOME/$VENV_NAME"} VENV_BIN="$VENV_PATH/bin" -LE_AUTO_VERSION="0.8.0" +LE_AUTO_VERSION="0.8.1" BASENAME=$(basename $0) USAGE="Usage: $BASENAME [OPTIONS] A self-updating wrapper script for the Certbot ACME client. When run, updates @@ -172,7 +172,7 @@ BootstrapDebCommon() { # distro version (#346) virtualenv= - if apt-cache show virtualenv > /dev/null 2>&1; then + if apt-cache show virtualenv > /dev/null 2>&1 && ! apt-cache --quiet=0 show virtualenv 2>&1 | grep -q 'No packages found'; then virtualenv="virtualenv" fi @@ -458,12 +458,39 @@ BootstrapSmartOS() { pkgin -y install 'gcc49' 'py27-augeas' 'py27-virtualenv' } +BootstrapMageiaCommon() { + if ! $SUDO urpmi --force \ + python \ + libpython-devel \ + python-virtualenv + then + echo "Could not install Python dependencies. Aborting bootstrap!" + exit 1 + fi + + if ! $SUDO urpmi --force \ + git \ + gcc \ + cdialog \ + python-augeas \ + libopenssl-devel \ + libffi-devel \ + rootcerts + then + echo "Could not install additional dependencies. Aborting bootstrap!" + exit 1 + fi +} + # Install required OS packages: Bootstrap() { if [ -f /etc/debian_version ]; then echo "Bootstrapping dependencies for Debian-based OSes..." BootstrapDebCommon + elif [ -f /etc/mageia-release ] ; then + # Mageia has both /etc/mageia-release and /etc/redhat-release + ExperimentalBootstrap "Mageia" BootstrapMageiaCommon elif [ -f /etc/redhat-release ]; then echo "Bootstrapping dependencies for RedHat-based OSes..." BootstrapRpmCommon @@ -476,7 +503,7 @@ Bootstrap() { BootstrapArchCommon else echo "Please use pacman to install letsencrypt packages:" - echo "# pacman -S letsencrypt letsencrypt-apache" + echo "# pacman -S certbot certbot-apache" echo echo "If you would like to use the virtualenv way, please run the script again with the" echo "--debug flag." @@ -500,6 +527,7 @@ Bootstrap() { echo "You will need to bootstrap, configure virtualenv, and run pip install manually." echo "Please see https://letsencrypt.readthedocs.org/en/latest/contributing.html#prerequisites" echo "for more info." + exit 1 fi } @@ -719,15 +747,15 @@ letsencrypt==0.7.0 \ # THE LINES BELOW ARE EDITED BY THE RELEASE SCRIPT; ADD ALL DEPENDENCIES ABOVE. -acme==0.8.0 \ - --hash=sha256:8561d590e496afb41a8ff2dac389199661d9cd785b1636ae08325771511189af \ - --hash=sha256:dfa86b547628b231f275c7e0efc7a09bec5dfaec866f89f5c5b59b78c14564da -certbot==0.8.0 \ - --hash=sha256:395c5840ff6b75aa51ee6449c86d016c14c5f65a71281e7bcef5feecac6a3293 \ - --hash=sha256:3c3c70b484fb3243a166515adc81ae0401c5d687a2763c75b40df9d8241a4314 -certbot-apache==0.8.0 \ - --hash=sha256:f4d4fc962ecc19646f6745d49c62a265d26e5b2df3acf34ef4865351594156e3 \ - --hash=sha256:cfb211debbcb0d0645c88d7e8bb38c591fca263bfdb5337242c023956055e268 +acme==0.8.1 \ + --hash=sha256:ccd7883772efbf933f91713b8241455993834f3620c8fbd459d9ed5e50bbaaca \ + --hash=sha256:d3ea4acf280bf6253ad7d641cb0970f230a19805acfed809e7a8ddcf62157d9f +certbot==0.8.1 \ + --hash=sha256:89805d9f70249ae859ec4d7a99c00b4bb7083ca90cd12d4d202b76dfc284f7c5 \ + --hash=sha256:6ca8df3d310ced6687d38aac17c0fb8c1b2ec7a3bea156a254e4cc2a1c132771 +certbot-apache==0.8.1 \ + --hash=sha256:c9e3fdc15e65589c2e39eb0e6b1f61f0c0a1db3c17b00bb337f0ff636cc61cb3 \ + --hash=sha256:0faf2879884d3b7a58b071902fba37d4b8b58a50e2c3b8ac262c0a74134045ed UNLIKELY_EOF # ------------------------------------------------------------------------- diff --git a/letsencrypt-auto-source/certbot-auto.asc b/letsencrypt-auto-source/certbot-auto.asc index 0255229b0..5bb725c96 100644 --- a/letsencrypt-auto-source/certbot-auto.asc +++ b/letsencrypt-auto-source/certbot-auto.asc @@ -1,11 +1,11 @@ -----BEGIN PGP SIGNATURE----- Version: GnuPG v1 -iQEcBAABAgAGBQJXUJvwAAoJEE0XyZXNl3XyvKsH/3qn7Xa/GQx3HvB6Io/Csn/E -v1nbUg5RPwvrTyyol8BJ6UrHiJw+gTbUgCAnBkZ7DYKaC8AQmQXVRcWXNALMMTzB -6LpBXjQQ2xrBYamGj70N7KnTM1QmxI96GUQouiHMJVugV4uihKJDjtR8/f2JWKok -ZSox6E4LqC45HzqLWiOqc13TrHbti32Mo8DyC63PBnSwMnypGLK6XcqM0L9Re62W -smoKu1VWKwWZYRYXIQr0dvK4JmVTrIsdASdZkhTC/vc8y4tGkdN0DcF2EHzci6OA -Tx0W+Ao+HM1ZcaaH3BJ1y3kYfT+mlt6o4OaK3UB/wtUzMmVih7l1UeiNkVL0oYk= -=t3L6 +iQEcBAABAgAGBQJXYJmBAAoJEE0XyZXNl3XyyIMH/jtYFb7rl5XXN8hjlKuK5frq +z7/jdK7fvI+mtYJ4i2Cy3yMz8T4wscXGkhxNtipbATWlpevPfjYzm4ZGC25coFZx +fDX44w0hBBgel7EISXGR1ABXb2rj24TZxIYXwaeClylsK9n5CxcWBocn8tDlfr8t +7VQUJEL3l1IlrnKnvpoL4Eq11sxlIPtitDPJ5c98ZM1293ZbWzIqyZKoXLIUkKHg +pkaa80j/QMmFumxzXFenU91JusLdeoblvjjg+kzjGonjslAYIuH4wEEjz2VJuUYe +P2+2ZyW4eLA6rRZhZ3CMtV79HzTPTWiELCYbXezb+yXJJEqzCYtIXkmbNQ3jUEY= +=86lB -----END PGP SIGNATURE----- diff --git a/letsencrypt-auto-source/letsencrypt-auto b/letsencrypt-auto-source/letsencrypt-auto index 1e3118848..80f39cf59 100755 --- a/letsencrypt-auto-source/letsencrypt-auto +++ b/letsencrypt-auto-source/letsencrypt-auto @@ -19,7 +19,7 @@ XDG_DATA_HOME=${XDG_DATA_HOME:-~/.local/share} VENV_NAME="letsencrypt" VENV_PATH=${VENV_PATH:-"$XDG_DATA_HOME/$VENV_NAME"} VENV_BIN="$VENV_PATH/bin" -LE_AUTO_VERSION="0.9.0.dev0" +LE_AUTO_VERSION="0.8.1" BASENAME=$(basename $0) USAGE="Usage: $BASENAME [OPTIONS] A self-updating wrapper script for the Certbot ACME client. When run, updates @@ -747,15 +747,15 @@ letsencrypt==0.7.0 \ # THE LINES BELOW ARE EDITED BY THE RELEASE SCRIPT; ADD ALL DEPENDENCIES ABOVE. -acme==0.8.0 \ - --hash=sha256:8561d590e496afb41a8ff2dac389199661d9cd785b1636ae08325771511189af \ - --hash=sha256:dfa86b547628b231f275c7e0efc7a09bec5dfaec866f89f5c5b59b78c14564da -certbot==0.8.0 \ - --hash=sha256:395c5840ff6b75aa51ee6449c86d016c14c5f65a71281e7bcef5feecac6a3293 \ - --hash=sha256:3c3c70b484fb3243a166515adc81ae0401c5d687a2763c75b40df9d8241a4314 -certbot-apache==0.8.0 \ - --hash=sha256:f4d4fc962ecc19646f6745d49c62a265d26e5b2df3acf34ef4865351594156e3 \ - --hash=sha256:cfb211debbcb0d0645c88d7e8bb38c591fca263bfdb5337242c023956055e268 +acme==0.8.1 \ + --hash=sha256:ccd7883772efbf933f91713b8241455993834f3620c8fbd459d9ed5e50bbaaca \ + --hash=sha256:d3ea4acf280bf6253ad7d641cb0970f230a19805acfed809e7a8ddcf62157d9f +certbot==0.8.1 \ + --hash=sha256:89805d9f70249ae859ec4d7a99c00b4bb7083ca90cd12d4d202b76dfc284f7c5 \ + --hash=sha256:6ca8df3d310ced6687d38aac17c0fb8c1b2ec7a3bea156a254e4cc2a1c132771 +certbot-apache==0.8.1 \ + --hash=sha256:c9e3fdc15e65589c2e39eb0e6b1f61f0c0a1db3c17b00bb337f0ff636cc61cb3 \ + --hash=sha256:0faf2879884d3b7a58b071902fba37d4b8b58a50e2c3b8ac262c0a74134045ed UNLIKELY_EOF # ------------------------------------------------------------------------- diff --git a/letsencrypt-auto-source/letsencrypt-auto.sig b/letsencrypt-auto-source/letsencrypt-auto.sig index f024577a34a42779e39755635eca4daf6f46ac2a..e3da0e0a35a6dafe26dbbe061f546aeb929dfcdb 100644 GIT binary patch literal 256 zcmV+b0ssDf@*{!ug8Wu1n(zcS_L8lFE=3`8xR7ru#Yq0;RMek)(d|5ow9N+eJQroy zYC^uLM9I-+sE5&5@?b;V8wXnSYWY@reJu{#UVlBrThxmxIb7 G&VdBMz>LuV literal 256 zcmV+b0ssE80bqRH{`qI)8kxNPxBYvrI}8PM??lS5W%Vrx#G#77ah$EmwK78hzI8fL zpFoy+Z$Ff?%2R#7_m4mldi$VK+OCedjDk7*K$j_4-%~DH(I7GKai+aBwofiSx4Q27 z;SV#~5cO^_zi`sHUA566Xqu@Q&d>p~uV#$A5kiL^Wt5QWl@grCRt$tN>*5-L&1i=@?vr?8a-~$mStKTr+dwJTPqN4;)GA0wY_jCnH7Wesi1I|l7M@y GRi#to9)G0( diff --git a/letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt b/letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt index c33795ced..1291b2c96 100644 --- a/letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt +++ b/letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt @@ -181,12 +181,12 @@ letsencrypt==0.7.0 \ # THE LINES BELOW ARE EDITED BY THE RELEASE SCRIPT; ADD ALL DEPENDENCIES ABOVE. -acme==0.8.0 \ - --hash=sha256:8561d590e496afb41a8ff2dac389199661d9cd785b1636ae08325771511189af \ - --hash=sha256:dfa86b547628b231f275c7e0efc7a09bec5dfaec866f89f5c5b59b78c14564da -certbot==0.8.0 \ - --hash=sha256:395c5840ff6b75aa51ee6449c86d016c14c5f65a71281e7bcef5feecac6a3293 \ - --hash=sha256:3c3c70b484fb3243a166515adc81ae0401c5d687a2763c75b40df9d8241a4314 -certbot-apache==0.8.0 \ - --hash=sha256:f4d4fc962ecc19646f6745d49c62a265d26e5b2df3acf34ef4865351594156e3 \ - --hash=sha256:cfb211debbcb0d0645c88d7e8bb38c591fca263bfdb5337242c023956055e268 +acme==0.8.1 \ + --hash=sha256:ccd7883772efbf933f91713b8241455993834f3620c8fbd459d9ed5e50bbaaca \ + --hash=sha256:d3ea4acf280bf6253ad7d641cb0970f230a19805acfed809e7a8ddcf62157d9f +certbot==0.8.1 \ + --hash=sha256:89805d9f70249ae859ec4d7a99c00b4bb7083ca90cd12d4d202b76dfc284f7c5 \ + --hash=sha256:6ca8df3d310ced6687d38aac17c0fb8c1b2ec7a3bea156a254e4cc2a1c132771 +certbot-apache==0.8.1 \ + --hash=sha256:c9e3fdc15e65589c2e39eb0e6b1f61f0c0a1db3c17b00bb337f0ff636cc61cb3 \ + --hash=sha256:0faf2879884d3b7a58b071902fba37d4b8b58a50e2c3b8ac262c0a74134045ed From 5c74e728b584e3750ba2b67d057339e8749faecd Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Tue, 14 Jun 2016 16:56:31 -0700 Subject: [PATCH 319/396] Bump version to 0.9.0 --- acme/setup.py | 2 +- certbot-apache/setup.py | 2 +- certbot-compatibility-test/setup.py | 2 +- certbot-nginx/setup.py | 2 +- certbot/__init__.py | 2 +- letsencrypt-auto-source/letsencrypt-auto | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/acme/setup.py b/acme/setup.py index 2bcec9128..ed133e128 100644 --- a/acme/setup.py +++ b/acme/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.8.1' +version = '0.9.0.dev0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot-apache/setup.py b/certbot-apache/setup.py index 87214d2e5..e3dbe4563 100644 --- a/certbot-apache/setup.py +++ b/certbot-apache/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.8.1' +version = '0.9.0.dev0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot-compatibility-test/setup.py b/certbot-compatibility-test/setup.py index cd282af50..fb56be65f 100644 --- a/certbot-compatibility-test/setup.py +++ b/certbot-compatibility-test/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.8.1' +version = '0.9.0.dev0' install_requires = [ 'certbot', diff --git a/certbot-nginx/setup.py b/certbot-nginx/setup.py index f76cbe596..62c705b4c 100644 --- a/certbot-nginx/setup.py +++ b/certbot-nginx/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.8.1' +version = '0.9.0.dev0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot/__init__.py b/certbot/__init__.py index e5d5629b1..34358a5d9 100644 --- a/certbot/__init__.py +++ b/certbot/__init__.py @@ -1,4 +1,4 @@ """Certbot client.""" # version number like 1.2.3a0, must have at least 2 parts, like 1.2 -__version__ = '0.8.1' +__version__ = '0.9.0.dev0' diff --git a/letsencrypt-auto-source/letsencrypt-auto b/letsencrypt-auto-source/letsencrypt-auto index 80f39cf59..80b81c898 100755 --- a/letsencrypt-auto-source/letsencrypt-auto +++ b/letsencrypt-auto-source/letsencrypt-auto @@ -19,7 +19,7 @@ XDG_DATA_HOME=${XDG_DATA_HOME:-~/.local/share} VENV_NAME="letsencrypt" VENV_PATH=${VENV_PATH:-"$XDG_DATA_HOME/$VENV_NAME"} VENV_BIN="$VENV_PATH/bin" -LE_AUTO_VERSION="0.8.1" +LE_AUTO_VERSION="0.9.0.dev0" BASENAME=$(basename $0) USAGE="Usage: $BASENAME [OPTIONS] A self-updating wrapper script for the Certbot ACME client. When run, updates From 176751f4da939c88741c5438b0ec649e1e9cfbbf Mon Sep 17 00:00:00 2001 From: Jacob Sachs Date: Tue, 14 Jun 2016 23:15:55 -0400 Subject: [PATCH 320/396] Log new cert and cert renewal --- certbot/main.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/certbot/main.py b/certbot/main.py index f68373998..7d6830b18 100644 --- a/certbot/main.py +++ b/certbot/main.py @@ -88,9 +88,11 @@ def _auth_from_domains(le_client, config, domains, lineage=None): hooks.pre_hook(config) try: if action == "renew": + logger.info("Renewing an existing certificate") renewal.renew_cert(config, domains, le_client, lineage) elif action == "newcert": # TREAT AS NEW REQUEST + logger.info("Obtaining a new certificate") lineage = le_client.obtain_and_enroll_certificate(domains) if lineage is False: raise errors.Error("Certificate could not be obtained") From 98a2e0c6d6a519b251f23735d83a40244eab3349 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Wed, 15 Jun 2016 16:02:33 -0700 Subject: [PATCH 321/396] Another waypoint --- certbot-nginx/certbot_nginx/nginxparser.py | 20 ++++++++++++-------- certbot-nginx/certbot_nginx/tls_sni_01.py | 2 +- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/certbot-nginx/certbot_nginx/nginxparser.py b/certbot-nginx/certbot_nginx/nginxparser.py index 13498bee2..b30a71bea 100644 --- a/certbot-nginx/certbot_nginx/nginxparser.py +++ b/certbot-nginx/certbot_nginx/nginxparser.py @@ -18,8 +18,10 @@ class RawNginxParser(object): left_bracket = Literal("{").suppress() right_bracket = Literal("}").suppress() semicolon = Literal(";").suppress() - space = Optional(White()) key = Word(alphanums + "_/+-.") + space = Optional(White()) + nspace = Optional(Regex(r"[ \t]+")) + blankLine = ZeroOrMore(Regex(r"^\S$")) # Matches anything that is not a special character AND any chars in single # or double quotes value = Regex(r"((\".*\")?(\'.*\')?[^\{\};,]?)+") @@ -31,23 +33,25 @@ class RawNginxParser(object): comment = space + Literal('#') + restOfLine() assignment = space + key + Optional(space + value, default=None) + semicolon - location_statement = space + Optional(modifier) + Optional(space + location) + location_statement = space + Optional(modifier) + Optional(space + location + space) if_statement = space + Literal("if") + space + Regex(r"\(.+\)") + space map_statement = space + Literal("map") + space + Regex(r"\S+") + space + Regex(r"\$\S+") + space block = Forward() block << Group( # XXX could this "key" be Literal("location")? - (Group(key + location_statement) ^ Group(if_statement) ^ Group(map_statement)) + + # WIP: in order to allow this leaveWhitespace(), we're going to need an explicit + # "whitespaceline" construction... + (Group(space + key + location_statement) ^ Group(if_statement) ^ + Group(map_statement)).leaveWhitespace() + left_bracket + Group(ZeroOrMore(Group(comment | assignment) | block)) + - right_bracket) + space + right_bracket) script = OneOrMore(Group(comment | assignment) ^ block) + stringEnd script.parseWithTabs() - def __init__(self, source): - self.source = source + def __init__(self, source): self.source = source def parse(self): """Returns the parsed tree.""" @@ -74,7 +78,7 @@ class RawNginxDumper(object): indentation = "" if spacey(b[0]): indentation = b.pop(0) - indentation = indentation.replace("\n", "", 1) + #indentation = indentation.replace("\n", "", 1) key = b.pop(0) values = b.pop(0) @@ -103,7 +107,7 @@ class RawNginxDumper(object): def __str__(self): """Return the parsed block as a string.""" print "Merging:\n", '\n'.join(self) - return '\n'.join(self) + '\n' + return ''.join(self) + '\n' diff --git a/certbot-nginx/certbot_nginx/tls_sni_01.py b/certbot-nginx/certbot_nginx/tls_sni_01.py index efb5d53e6..d24cb830d 100644 --- a/certbot-nginx/certbot_nginx/tls_sni_01.py +++ b/certbot-nginx/certbot_nginx/tls_sni_01.py @@ -124,9 +124,9 @@ class NginxTlsSni01(common.TLSSNI01): True, self.challenge_conf) with open(self.challenge_conf, "w") as new_conf: + out = nginxparser.dumps(config) if "mime" in self.challenge_conf: print "Weird" - out = nginxparser.dumps(config) print out #sys.exit(1) #nginxparser.dump(config, new_conf) From 5e59b8ad46872657f9a6fd0a12ae1f1dd8eceaf7 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Wed, 15 Jun 2016 16:42:47 -0700 Subject: [PATCH 322/396] Woohoo! it works --- certbot-nginx/certbot_nginx/nginxparser.py | 28 +++++++++++++++------- certbot-nginx/certbot_nginx/parser.py | 2 +- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/certbot-nginx/certbot_nginx/nginxparser.py b/certbot-nginx/certbot_nginx/nginxparser.py index b30a71bea..c8893f80a 100644 --- a/certbot-nginx/certbot_nginx/nginxparser.py +++ b/certbot-nginx/certbot_nginx/nginxparser.py @@ -15,12 +15,12 @@ class RawNginxParser(object): """A class that parses nginx configuration with pyparsing.""" # constants - left_bracket = Literal("{").suppress() - right_bracket = Literal("}").suppress() - semicolon = Literal(";").suppress() - key = Word(alphanums + "_/+-.") space = Optional(White()) nspace = Optional(Regex(r"[ \t]+")) + left_bracket = Literal("{").suppress() + right_bracket = space.leaveWhitespace() + Literal("}").suppress() + semicolon = Literal(";").suppress() + key = Word(alphanums + "_/+-.") blankLine = ZeroOrMore(Regex(r"^\S$")) # Matches anything that is not a special character AND any chars in single # or double quotes @@ -45,8 +45,8 @@ class RawNginxParser(object): (Group(space + key + location_statement) ^ Group(if_statement) ^ Group(map_statement)).leaveWhitespace() + left_bracket + - Group(ZeroOrMore(Group(comment | assignment) | block)) + - space + right_bracket) + Group(ZeroOrMore(Group(comment | assignment) | block) + space).leaveWhitespace() + + right_bracket) script = OneOrMore(Group(comment | assignment) ^ block) + stringEnd script.parseWithTabs() @@ -76,11 +76,22 @@ class RawNginxDumper(object): b0 = b b = copy.deepcopy(b) indentation = "" + if isinstance(b, str): + yield b + continue if spacey(b[0]): - indentation = b.pop(0) + try: + indentation = b.pop(0) + except: + import ipdb + ipdb.set_trace() + #indentation = indentation.replace("\n", "", 1) key = b.pop(0) values = b.pop(0) + if "worker_processes" in str(b0) and False: + import ipdb + ipdb.set_trace() if isinstance(key, list): yield indentation + "".join(key) + '{' @@ -106,7 +117,8 @@ class RawNginxDumper(object): def __str__(self): """Return the parsed block as a string.""" - print "Merging:\n", '\n'.join(self) + x = ''.join(self) + print "Merging:\n", repr(x) return ''.join(self) + '\n' diff --git a/certbot-nginx/certbot_nginx/parser.py b/certbot-nginx/certbot_nginx/parser.py index 642702c9f..61f728d2f 100644 --- a/certbot-nginx/certbot_nginx/parser.py +++ b/certbot-nginx/certbot_nginx/parser.py @@ -219,7 +219,7 @@ class NginxParser(object): with open(filename, 'w') as _file: _file.write(out) - if "owncloud" in filename: + if True or "owncloud" in filename: print "Outputting", filename print out a = open("/tmp/nginx/sites-enabled/owncloud.conf").read() From 4f46289c1b9b582f6ad90db0a5651e39d1b2f929 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Wed, 15 Jun 2016 17:26:38 -0700 Subject: [PATCH 323/396] Start cleanup --- certbot-nginx/certbot_nginx/nginxparser.py | 44 +++------------------- certbot-nginx/certbot_nginx/parser.py | 17 +-------- certbot-nginx/certbot_nginx/tls_sni_01.py | 5 --- 3 files changed, 7 insertions(+), 59 deletions(-) diff --git a/certbot-nginx/certbot_nginx/nginxparser.py b/certbot-nginx/certbot_nginx/nginxparser.py index c8893f80a..d1d17493a 100644 --- a/certbot-nginx/certbot_nginx/nginxparser.py +++ b/certbot-nginx/certbot_nginx/nginxparser.py @@ -16,12 +16,10 @@ class RawNginxParser(object): # constants space = Optional(White()) - nspace = Optional(Regex(r"[ \t]+")) left_bracket = Literal("{").suppress() right_bracket = space.leaveWhitespace() + Literal("}").suppress() semicolon = Literal(";").suppress() key = Word(alphanums + "_/+-.") - blankLine = ZeroOrMore(Regex(r"^\S$")) # Matches anything that is not a special character AND any chars in single # or double quotes value = Regex(r"((\".*\")?(\'.*\')?[^\{\};,]?)+") @@ -40,8 +38,6 @@ class RawNginxParser(object): block << Group( # XXX could this "key" be Literal("location")? - # WIP: in order to allow this leaveWhitespace(), we're going to need an explicit - # "whitespaceline" construction... (Group(space + key + location_statement) ^ Group(if_statement) ^ Group(map_statement)).leaveWhitespace() + left_bracket + @@ -51,7 +47,8 @@ class RawNginxParser(object): script = OneOrMore(Group(comment | assignment) ^ block) + stringEnd script.parseWithTabs() - def __init__(self, source): self.source = source + def __init__(self, source): + self.source = source def parse(self): """Returns the parsed tree.""" @@ -71,27 +68,16 @@ class RawNginxDumper(object): def __iter__(self, blocks=None): """Iterates the dumped nginx content.""" blocks = blocks or self.blocks - print "iterating", blocks for b in blocks: - b0 = b - b = copy.deepcopy(b) - indentation = "" if isinstance(b, str): yield b continue + b = copy.deepcopy(b) + indentation = "" if spacey(b[0]): - try: - indentation = b.pop(0) - except: - import ipdb - ipdb.set_trace() - - #indentation = indentation.replace("\n", "", 1) + indentation = b.pop(0) key = b.pop(0) values = b.pop(0) - if "worker_processes" in str(b0) and False: - import ipdb - ipdb.set_trace() if isinstance(key, list): yield indentation + "".join(key) + '{' @@ -117,14 +103,9 @@ class RawNginxDumper(object): def __str__(self): """Return the parsed block as a string.""" - x = ''.join(self) - print "Merging:\n", repr(x) return ''.join(self) + '\n' - - - # Shortcut functions to respect Python's serialization interface # (like pyyaml, picker or json) @@ -136,16 +117,7 @@ def loads(source): :rtype: list """ - new = UnspacedList(RawNginxParser(source).as_list()) - print "\nNew:" - print new - for entry in new: - print len(entry), " ", - print "\nNewspaced:" - for entry in new.spaced: - print str(len(entry))+ " ", - print "\ngo" - return new + return UnspacedList(RawNginxParser(source).as_list()) def load(_file): @@ -238,7 +210,3 @@ class UnspacedList(list): idx -= 1 pos += 1 return spaces - - def with_spaces(self): - return self.spaced - diff --git a/certbot-nginx/certbot_nginx/parser.py b/certbot-nginx/certbot_nginx/parser.py index 61f728d2f..18301de0e 100644 --- a/certbot-nginx/certbot_nginx/parser.py +++ b/certbot-nginx/certbot_nginx/parser.py @@ -215,25 +215,10 @@ class NginxParser(object): filename = filename + os.path.extsep + ext try: out = nginxparser.dumps(tree) - logger.debug('Dumping to %s:\n%s', filename, out) + #logger.debug('Writing nginx conf tree to %s:\n%s', filename, out) with open(filename, 'w') as _file: _file.write(out) - if True or "owncloud" in filename: - print "Outputting", filename - print out - a = open("/tmp/nginx/sites-enabled/owncloud.conf").read() - b = open(filename).read() - for linea, lineb in zip(a.split('\n'), b.split('\n')): - if linea != lineb: - print "a", repr(linea) - print "b", repr(lineb) - if a != b: - print "Mismatch!" - if a != out: - print "Double mismatch", len(a), len(out) - else: - print "Match!" except IOError: logger.error("Could not open file for writing: %s", filename) diff --git a/certbot-nginx/certbot_nginx/tls_sni_01.py b/certbot-nginx/certbot_nginx/tls_sni_01.py index d24cb830d..13ae2c358 100644 --- a/certbot-nginx/certbot_nginx/tls_sni_01.py +++ b/certbot-nginx/certbot_nginx/tls_sni_01.py @@ -125,11 +125,6 @@ class NginxTlsSni01(common.TLSSNI01): with open(self.challenge_conf, "w") as new_conf: out = nginxparser.dumps(config) - if "mime" in self.challenge_conf: - print "Weird" - print out - #sys.exit(1) - #nginxparser.dump(config, new_conf) new_conf.write(out) def _make_server_block(self, achall, addrs): From 2cbd680bd543c91f846c98d3bd9972c9d9105b4c Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Wed, 15 Jun 2016 17:36:53 -0700 Subject: [PATCH 324/396] Hide .spaced from users outside nginxparser.py --- certbot-nginx/certbot_nginx/nginxparser.py | 2 +- certbot-nginx/certbot_nginx/parser.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/certbot-nginx/certbot_nginx/nginxparser.py b/certbot-nginx/certbot_nginx/nginxparser.py index d1d17493a..272e39bbc 100644 --- a/certbot-nginx/certbot_nginx/nginxparser.py +++ b/certbot-nginx/certbot_nginx/nginxparser.py @@ -139,7 +139,7 @@ def dumps(blocks): :rtype: str """ - return str(RawNginxDumper(blocks)) + return str(RawNginxDumper(blocks.spaced)) def dump(blocks, _file): diff --git a/certbot-nginx/certbot_nginx/parser.py b/certbot-nginx/certbot_nginx/parser.py index 18301de0e..f2faadd58 100644 --- a/certbot-nginx/certbot_nginx/parser.py +++ b/certbot-nginx/certbot_nginx/parser.py @@ -210,7 +210,7 @@ class NginxParser(object): """ for filename in self.parsed: - tree = self.parsed[filename].spaced + tree = self.parsed[filename] if ext: filename = filename + os.path.extsep + ext try: From ff7addefb399ff5ef451a2590755c3976942f101 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Wed, 15 Jun 2016 17:57:24 -0700 Subject: [PATCH 325/396] Start fixing tests --- certbot-nginx/certbot_nginx/nginxparser.py | 4 ++-- .../certbot_nginx/tests/nginxparser_test.py | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/certbot-nginx/certbot_nginx/nginxparser.py b/certbot-nginx/certbot_nginx/nginxparser.py index 272e39bbc..b2f785d0e 100644 --- a/certbot-nginx/certbot_nginx/nginxparser.py +++ b/certbot-nginx/certbot_nginx/nginxparser.py @@ -22,7 +22,7 @@ class RawNginxParser(object): key = Word(alphanums + "_/+-.") # Matches anything that is not a special character AND any chars in single # or double quotes - value = Regex(r"((\".*\")?(\'.*\')?[^\{\};,]?)+") + value = Regex(r"((\".*\")?(\'.*\')?[^\{\};, ]?)+") location = CharsNotIn("{};," + string.whitespace) # modifier for location uri [ = | ~ | ~* | ^~ ] modifier = Literal("=") | Literal("~*") | Literal("~") | Literal("^~") @@ -30,7 +30,7 @@ class RawNginxParser(object): # rules comment = space + Literal('#') + restOfLine() - assignment = space + key + Optional(space + value, default=None) + semicolon + assignment = space + key + Optional(space + value, default=None) + space + semicolon location_statement = space + Optional(modifier) + Optional(space + location + space) if_statement = space + Literal("if") + space + Regex(r"\(.+\)") + space map_statement = space + Literal("map") + space + Regex(r"\S+") + space + Regex(r"\$\S+") + space diff --git a/certbot-nginx/certbot_nginx/tests/nginxparser_test.py b/certbot-nginx/certbot_nginx/tests/nginxparser_test.py index 80e82c903..0a09b4a64 100644 --- a/certbot-nginx/certbot_nginx/tests/nginxparser_test.py +++ b/certbot-nginx/certbot_nginx/tests/nginxparser_test.py @@ -17,23 +17,23 @@ class TestRawNginxParser(unittest.TestCase): def test_assignments(self): parsed = RawNginxParser.assignment.parseString('root /test;').asList() - self.assertEqual(parsed, ['root', '/test']) + self.assertEqual(parsed, ['root', ' ', '/test']) parsed = RawNginxParser.assignment.parseString('root /test;' 'foo bar;').asList() self.assertEqual(parsed, ['root', '/test'], ['foo', 'bar']) def test_blocks(self): parsed = RawNginxParser.block.parseString('foo {}').asList() - self.assertEqual(parsed, [[['foo'], []]]) + self.assertEqual(parsed, [[['foo', ' '], []]]) parsed = RawNginxParser.block.parseString('location /foo{}').asList() - self.assertEqual(parsed, [[['location', '/foo'], []]]) - parsed = RawNginxParser.block.parseString('foo { bar foo; }').asList() - self.assertEqual(parsed, [[['foo'], [['bar', 'foo']]]]) + self.assertEqual(parsed, [[['location', ' ', '/foo'], []]]) + parsed = RawNginxParser.block.parseString('foo { bar foo ; }').asList() + self.assertEqual(parsed, [[['foo', ' '], [[' ', 'bar', ' ', 'foo', ' '], ' ']]]) def test_nested_blocks(self): parsed = RawNginxParser.block.parseString('foo { bar {} }').asList() block, content = FIRST(parsed) - self.assertEqual(FIRST(content), [['bar'], []]) + self.assertEqual(FIRST(content), [[' ', 'bar', ' '], []]) self.assertEqual(FIRST(block), 'foo') def test_dump_as_string(self): From e5ce03b312c48b43136628b3fd4a756dc04d754c Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Thu, 16 Jun 2016 12:24:36 -0700 Subject: [PATCH 326/396] More test wrangling --- certbot-nginx/certbot_nginx/nginxparser.py | 4 ++-- .../certbot_nginx/tests/nginxparser_test.py | 24 ++++++++++++------- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/certbot-nginx/certbot_nginx/nginxparser.py b/certbot-nginx/certbot_nginx/nginxparser.py index b2f785d0e..272e39bbc 100644 --- a/certbot-nginx/certbot_nginx/nginxparser.py +++ b/certbot-nginx/certbot_nginx/nginxparser.py @@ -22,7 +22,7 @@ class RawNginxParser(object): key = Word(alphanums + "_/+-.") # Matches anything that is not a special character AND any chars in single # or double quotes - value = Regex(r"((\".*\")?(\'.*\')?[^\{\};, ]?)+") + value = Regex(r"((\".*\")?(\'.*\')?[^\{\};,]?)+") location = CharsNotIn("{};," + string.whitespace) # modifier for location uri [ = | ~ | ~* | ^~ ] modifier = Literal("=") | Literal("~*") | Literal("~") | Literal("^~") @@ -30,7 +30,7 @@ class RawNginxParser(object): # rules comment = space + Literal('#') + restOfLine() - assignment = space + key + Optional(space + value, default=None) + space + semicolon + assignment = space + key + Optional(space + value, default=None) + semicolon location_statement = space + Optional(modifier) + Optional(space + location + space) if_statement = space + Literal("if") + space + Regex(r"\(.+\)") + space map_statement = space + Literal("map") + space + Regex(r"\S+") + space + Regex(r"\$\S+") + space diff --git a/certbot-nginx/certbot_nginx/tests/nginxparser_test.py b/certbot-nginx/certbot_nginx/tests/nginxparser_test.py index 0a09b4a64..2a8558bfb 100644 --- a/certbot-nginx/certbot_nginx/tests/nginxparser_test.py +++ b/certbot-nginx/certbot_nginx/tests/nginxparser_test.py @@ -5,7 +5,7 @@ import unittest from pyparsing import ParseException from certbot_nginx.nginxparser import ( - RawNginxParser, loads, load, dumps, dump) + RawNginxParser, loads, load, dumps, dump, UnspacedList) from certbot_nginx.tests import util @@ -18,9 +18,8 @@ class TestRawNginxParser(unittest.TestCase): def test_assignments(self): parsed = RawNginxParser.assignment.parseString('root /test;').asList() self.assertEqual(parsed, ['root', ' ', '/test']) - parsed = RawNginxParser.assignment.parseString('root /test;' - 'foo bar;').asList() - self.assertEqual(parsed, ['root', '/test'], ['foo', 'bar']) + parsed = RawNginxParser.assignment.parseString('root /test;foo bar;').asList() + self.assertEqual(parsed, ['root', ' ', '/test'], ['foo', ' ', 'bar']) def test_blocks(self): parsed = RawNginxParser.block.parseString('foo {}').asList() @@ -28,7 +27,7 @@ class TestRawNginxParser(unittest.TestCase): parsed = RawNginxParser.block.parseString('location /foo{}').asList() self.assertEqual(parsed, [[['location', ' ', '/foo'], []]]) parsed = RawNginxParser.block.parseString('foo { bar foo ; }').asList() - self.assertEqual(parsed, [[['foo', ' '], [[' ', 'bar', ' ', 'foo', ' '], ' ']]]) + self.assertEqual(parsed, [[['foo', ' '], [[' ', 'bar', ' ', 'foo '], ' ']]]) def test_nested_blocks(self): parsed = RawNginxParser.block.parseString('foo { bar {} }').asList() @@ -116,7 +115,13 @@ class TestRawNginxParser(unittest.TestCase): def test_dump_as_file(self): with open(util.get_data_filename('nginx.conf')) as handle: - parsed = util.filter_comments(load(handle)) + try: + parsed = load(handle) + except: + handle.seek(0) + print "Failed on", handle.read() + raise + #parsed = util.filter_comments(parsed) parsed[-1][-1].append([['server'], [['listen', '443 ssl'], ['server_name', 'localhost'], @@ -128,12 +133,15 @@ class TestRawNginxParser(unittest.TestCase): [['location', '/'], [['root', 'html'], ['index', 'index.html index.htm']]]]]) + with open(util.get_data_filename('nginx.new.conf'), 'w') as handle: dump(parsed, handle) with open(util.get_data_filename('nginx.new.conf')) as handle: - parsed_new = util.filter_comments(load(handle)) - self.assertEquals(parsed, parsed_new) + parsed_new = load(handle) + self.maxDiff = None + self.assertEquals(parsed[0], parsed_new[0]) + self.assertEquals(parsed[1:], parsed_new[1:]) def test_comments(self): with open(util.get_data_filename('minimalistic_comments.conf')) as handle: From 72ba5b72cc2d11deb85385f9ca3813a4f0aecd2e Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Thu, 16 Jun 2016 12:25:11 -0700 Subject: [PATCH 327/396] Try to preserve the exact form of end-of-file whitespace --- certbot-nginx/certbot_nginx/nginxparser.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/certbot-nginx/certbot_nginx/nginxparser.py b/certbot-nginx/certbot_nginx/nginxparser.py index 272e39bbc..c8131072c 100644 --- a/certbot-nginx/certbot_nginx/nginxparser.py +++ b/certbot-nginx/certbot_nginx/nginxparser.py @@ -44,7 +44,7 @@ class RawNginxParser(object): Group(ZeroOrMore(Group(comment | assignment) | block) + space).leaveWhitespace() + right_bracket) - script = OneOrMore(Group(comment | assignment) ^ block) + stringEnd + script = OneOrMore(Group(comment | assignment) ^ block) + space + stringEnd script.parseWithTabs() def __init__(self, source): @@ -103,7 +103,7 @@ class RawNginxDumper(object): def __str__(self): """Return the parsed block as a string.""" - return ''.join(self) + '\n' + return ''.join(self) # Shortcut functions to respect Python's serialization interface From b82ebd9180db60333e87ea55318fa9c48490f200 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Thu, 16 Jun 2016 17:17:13 -0700 Subject: [PATCH 328/396] Fix desyncronisation with .spaced when modifying sublists - we now actually write directives again! --- certbot-nginx/certbot_nginx/configurator.py | 18 ++++----- certbot-nginx/certbot_nginx/nginxparser.py | 42 +++++++++++++++++---- certbot-nginx/certbot_nginx/parser.py | 8 +++- 3 files changed, 51 insertions(+), 17 deletions(-) diff --git a/certbot-nginx/certbot_nginx/configurator.py b/certbot-nginx/certbot_nginx/configurator.py index 30928e56c..4f46b6a66 100644 --- a/certbot-nginx/certbot_nginx/configurator.py +++ b/certbot-nginx/certbot_nginx/configurator.py @@ -152,17 +152,17 @@ class NginxConfigurator(common.Plugin): "install a cert.") vhost = self.choose_vhost(domain) - cert_directives = [['ssl_certificate', fullchain_path], - ['ssl_certificate_key', key_path]] + cert_directives = [['\n', 'ssl_certificate', ' ', fullchain_path], + ['\n', 'ssl_certificate_key', ' ', key_path]] # OCSP stapling was introduced in Nginx 1.3.7. If we have that version # or greater, add config settings for it. stapling_directives = [] if self.version >= (1, 3, 7): stapling_directives = [ - ['ssl_trusted_certificate', chain_path], - ['ssl_stapling', 'on'], - ['ssl_stapling_verify', 'on']] + ['\n', 'ssl_trusted_certificate', ' ', chain_path], + ['\n', 'ssl_stapling', ' ', 'on'], + ['\n', 'ssl_stapling_verify', ' ', 'on'], ['\n']] if len(stapling_directives) != 0 and not chain_path: raise errors.PluginError( @@ -337,10 +337,10 @@ class NginxConfigurator(common.Plugin): """ snakeoil_cert, snakeoil_key = self._get_snakeoil_paths() - ssl_block = [['listen', '{0} ssl'.format(self.config.tls_sni_01_port)], - ['ssl_certificate', snakeoil_cert], - ['ssl_certificate_key', snakeoil_key], - ['include', self.parser.loc["ssl_options"]]] + ssl_block = [['\n', 'listen', ' ', '{0} ssl'.format(self.config.tls_sni_01_port)], + ['\n', 'ssl_certificate', ' ', snakeoil_cert], + ['\n', 'ssl_certificate_key', ' ', snakeoil_key], + ['\n', 'include', ' ', self.parser.loc["ssl_options"]]] self.parser.add_server_directives( vhost.filep, vhost.names, ssl_block, replace=False) vhost.ssl = True diff --git a/certbot-nginx/certbot_nginx/nginxparser.py b/certbot-nginx/certbot_nginx/nginxparser.py index c8131072c..856113f71 100644 --- a/certbot-nginx/certbot_nginx/nginxparser.py +++ b/certbot-nginx/certbot_nginx/nginxparser.py @@ -76,6 +76,9 @@ class RawNginxDumper(object): indentation = "" if spacey(b[0]): indentation = b.pop(0) + if not b: + yield indentation + continue key = b.pop(0) values = b.pop(0) @@ -154,36 +157,58 @@ def dump(blocks, _file): return _file.write(dumps(blocks)) -spacey = lambda x: isinstance(x, str) and x.isspace() +spacey = lambda x: (isinstance(x, str) and x.isspace()) or x == '' class UnspacedList(list): """Wrap a list [of lists], making any whitespace entries magically invisible""" - def __init__(self, list_source): + def __init__(self, list_source, top=False): self.spaced = copy.deepcopy(list(list_source)) # Turn self into a version of the source list that has spaces removed # and all sub-lists also UnspacedList()ed list.__init__(self, list_source) + self.top = self for i, entry in reversed(list(enumerate(self))): if isinstance(entry, list): - list.__setitem__(self, i, UnspacedList(entry)) + sublist = UnspacedList(entry, top=self.top) + list.__setitem__(self, i, sublist) + assert type(self.spaced) == list, "Type madness %r" % type(self.spaced) + self.spaced[i] = sublist.spaced elif spacey(entry): list.__delitem__(self, i) def insert(self, i, x): - self.spaced.insert(i + self._spaces_before(i), x) + if hasattr(x, "spaced"): + self.spaced.insert(i + self._spaces_before(i), x.spaced) + else: + self.spaced.insert(i + self._spaces_before(i), x) list.insert(self, i, x) def append(self, x): - self.spaced.append(x) + print "Unspaced append", x, self + if hasattr(x, "spaced"): + self.spaced.append(x.spaced) + else: + self.spaced.append(x) list.append(self, x) + print "After: aaaaaaaaaaaaaaaaa" + print self.top + print "Aftertop: bbbbbbbbbbbbbbbbb" + print self.top.spaced + #import ipdb + #ipdb.set_trace() def extend(self, x): - self.spaced.extend(x) + if hasattr(x, "spaced"): + self.spaced.extend(x.spaced) + else: + self.spaced.extend(x) + self.logger.debug("Weird, extending regular list %r to Unspaced %r", x, self) list.extend(self, x) def __add__(self, other): + print "Unspaced add", self, other if hasattr(other, "spaced"): # If the thing added to us is an UnspacedList, use its spaced form self.spaced.__add__(other.spaced) @@ -192,7 +217,10 @@ class UnspacedList(list): list.__add__(self, other) def __setitem__(self, i, value): - self.spaced.__setitem__(i + self._spaces_before(i), value) + if hasattr(value, "spaced"): + self.spaced.__setitem__(i + self._spaces_before(i), value.spaced) + else: + self.spaced.__setitem__(i + self._spaces_before(i), value) list.__setitem__(self, i, value) def __delitem__(self, i): diff --git a/certbot-nginx/certbot_nginx/parser.py b/certbot-nginx/certbot_nginx/parser.py index f2faadd58..61c719816 100644 --- a/certbot-nginx/certbot_nginx/parser.py +++ b/certbot-nginx/certbot_nginx/parser.py @@ -215,7 +215,7 @@ class NginxParser(object): filename = filename + os.path.extsep + ext try: out = nginxparser.dumps(tree) - #logger.debug('Writing nginx conf tree to %s:\n%s', filename, out) + logger.debug('Writing nginx conf tree to %s:\n%s', filename, out) with open(filename, 'w') as _file: _file.write(out) @@ -506,6 +506,12 @@ def _add_directive(block, directive, replace): See _add_directives for more documentation. """ + directive = nginxparser.UnspacedList(directive) + print "Unspacified", directive.spaced, directive + if len(directive) == 0: + # whitespace + block.append(directive) + return location = -1 # Find the index of a config line where the name of the directive matches # the name of the directive we want to add. From e76e3a953a81769ebdc1e9890dab6254cc48da96 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Thu, 16 Jun 2016 17:58:05 -0700 Subject: [PATCH 329/396] Fix test cases - but we're still mangling files in place... --- certbot-nginx/certbot_nginx/nginxparser.py | 37 ++++++------ .../certbot_nginx/tests/nginxparser_test.py | 57 +++++++++---------- 2 files changed, 43 insertions(+), 51 deletions(-) diff --git a/certbot-nginx/certbot_nginx/nginxparser.py b/certbot-nginx/certbot_nginx/nginxparser.py index 856113f71..d7248ad49 100644 --- a/certbot-nginx/certbot_nginx/nginxparser.py +++ b/certbot-nginx/certbot_nginx/nginxparser.py @@ -1,7 +1,6 @@ """Very low-level nginx config parser based on pyparsing.""" import copy import string -import sys from pyparsing import ( Literal, White, Word, alphanums, CharsNotIn, Forward, Group, @@ -68,11 +67,11 @@ class RawNginxDumper(object): def __iter__(self, blocks=None): """Iterates the dumped nginx content.""" blocks = blocks or self.blocks - for b in blocks: - if isinstance(b, str): - yield b + for b0 in blocks: + if isinstance(b0, str): + yield b0 continue - b = copy.deepcopy(b) + b = copy.deepcopy(b0) indentation = "" if spacey(b[0]): indentation = b.pop(0) @@ -96,13 +95,18 @@ class RawNginxDumper(object): gap = "" # Sometimes the parser has stuck some gap whitespace in here; # if so rotate it into gap - if spacey(values): + if values and spacey(values): gap = values - values = b.pop(0) - if values is None: - yield indentation + key + gap + ';' - else: - yield indentation + key + gap + values + ';' + try: + values = b.pop(0) + except: + import ipdb + ipdb.set_trace() + #if values is None: + # yield indentation + key + gap + ';' + #else: + # yield indentation + key + gap + values + ';' + yield indentation + key + gap + values + ';' def __str__(self): """Return the parsed block as a string.""" @@ -168,12 +172,11 @@ class UnspacedList(list): # Turn self into a version of the source list that has spaces removed # and all sub-lists also UnspacedList()ed list.__init__(self, list_source) - self.top = self + self.top = top if top else self for i, entry in reversed(list(enumerate(self))): if isinstance(entry, list): sublist = UnspacedList(entry, top=self.top) list.__setitem__(self, i, sublist) - assert type(self.spaced) == list, "Type madness %r" % type(self.spaced) self.spaced[i] = sublist.spaced elif spacey(entry): list.__delitem__(self, i) @@ -186,18 +189,11 @@ class UnspacedList(list): list.insert(self, i, x) def append(self, x): - print "Unspaced append", x, self if hasattr(x, "spaced"): self.spaced.append(x.spaced) else: self.spaced.append(x) list.append(self, x) - print "After: aaaaaaaaaaaaaaaaa" - print self.top - print "Aftertop: bbbbbbbbbbbbbbbbb" - print self.top.spaced - #import ipdb - #ipdb.set_trace() def extend(self, x): if hasattr(x, "spaced"): @@ -208,7 +204,6 @@ class UnspacedList(list): list.extend(self, x) def __add__(self, other): - print "Unspaced add", self, other if hasattr(other, "spaced"): # If the thing added to us is an UnspacedList, use its spaced form self.spaced.__add__(other.spaced) diff --git a/certbot-nginx/certbot_nginx/tests/nginxparser_test.py b/certbot-nginx/certbot_nginx/tests/nginxparser_test.py index 2a8558bfb..2efacd940 100644 --- a/certbot-nginx/certbot_nginx/tests/nginxparser_test.py +++ b/certbot-nginx/certbot_nginx/tests/nginxparser_test.py @@ -36,19 +36,20 @@ class TestRawNginxParser(unittest.TestCase): self.assertEqual(FIRST(block), 'foo') def test_dump_as_string(self): - dumped = dumps([ - ['user', 'www-data'], - [['server'], [ - ['listen', '80'], - ['server_name', 'foo.com'], - ['root', '/home/ubuntu/sites/foo/'], - [['location', '/status'], [ - ['check_status', None], - [['types'], [['image/jpeg', 'jpg']]], + dumped = dumps(UnspacedList([ + ['user', ' ', 'www-data'], + [['\n', 'server', ' '], [ + ['\n ', 'listen', ' ', '80'], + ['\n ', 'server_name', ' ', 'foo.com'], + ['\n ', 'root', ' ', '/home/ubuntu/sites/foo/'], + [['\n\n ', 'location', ' ', '/status', ' '], [ + ['\n ', 'check_status', ''], + [['\n\n ', 'types', ' '], + [['\n ', 'image/jpeg', ' ', 'jpg']]], ]] - ]]]) + ]]])) - self.assertEqual(dumped, + self.assertEqual(dumped.split('\n'), 'user www-data;\n' 'server {\n' ' listen 80;\n' @@ -59,10 +60,7 @@ class TestRawNginxParser(unittest.TestCase): ' check_status;\n' '\n' ' types {\n' - ' image/jpeg jpg;\n' - ' }\n' - ' }\n' - '}\n') + ' image/jpeg jpg;}}}'.split('\n')) def test_parse_from_file(self): with open(util.get_data_filename('foo.conf')) as handle: @@ -122,18 +120,17 @@ class TestRawNginxParser(unittest.TestCase): print "Failed on", handle.read() raise #parsed = util.filter_comments(parsed) - parsed[-1][-1].append([['server'], - [['listen', '443 ssl'], - ['server_name', 'localhost'], - ['ssl_certificate', 'cert.pem'], - ['ssl_certificate_key', 'cert.key'], - ['ssl_session_cache', 'shared:SSL:1m'], - ['ssl_session_timeout', '5m'], - ['ssl_ciphers', 'HIGH:!aNULL:!MD5'], - [['location', '/'], - [['root', 'html'], - ['index', 'index.html index.htm']]]]]) - + parsed[-1][-1].append(UnspacedList([['server'], + [['listen', ' ', '443 ssl'], + ['server_name', ' ', 'localhost'], + ['ssl_certificate', ' ', 'cert.pem'], + ['ssl_certificate_key', ' ', 'cert.key'], + ['ssl_session_cache', ' ', 'shared:SSL:1m'], + ['ssl_session_timeout', ' ', '5m'], + ['ssl_ciphers', ' ', 'HIGH:!aNULL:!MD5'], + [['location', ' ', '/'], + [['root', ' ', 'html'], + ['index', ' ', 'index.html index.htm']]]]])) with open(util.get_data_filename('nginx.new.conf'), 'w') as handle: dump(parsed, handle) @@ -159,11 +156,11 @@ class TestRawNginxParser(unittest.TestCase): ['#', " Use bar.conf when it's a full moon!"], ['include', 'foo.conf'], ['#', ' Kilroy was here'], - ['check_status', None], + ['check_status'], [['server'], - [['#', ''], + [['#'], ['#', " Don't forget to open up your firewall!"], - ['#', ''], + ['#'], ['listen', '1234'], ['#', ' listen 80;']]], ]) From da250a4f4bfb5e8feb6efda25745a03cbcd23041 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Thu, 16 Jun 2016 17:59:49 -0700 Subject: [PATCH 330/396] Experimentally delete these output files --- .../etc_nginx/minimalistic_comments.new.conf | 11 --- .../tests/testdata/etc_nginx/nginx.new.conf | 83 ------------------- 2 files changed, 94 deletions(-) delete mode 100644 certbot-nginx/certbot_nginx/tests/testdata/etc_nginx/minimalistic_comments.new.conf delete mode 100644 certbot-nginx/certbot_nginx/tests/testdata/etc_nginx/nginx.new.conf diff --git a/certbot-nginx/certbot_nginx/tests/testdata/etc_nginx/minimalistic_comments.new.conf b/certbot-nginx/certbot_nginx/tests/testdata/etc_nginx/minimalistic_comments.new.conf deleted file mode 100644 index d1b7be91e..000000000 --- a/certbot-nginx/certbot_nginx/tests/testdata/etc_nginx/minimalistic_comments.new.conf +++ /dev/null @@ -1,11 +0,0 @@ -# Use bar.conf when it's a full moon! -include foo.conf; -# Kilroy was here -check_status; -server { - # - # Don't forget to open up your firewall! - # - listen 1234; - # listen 80; -} diff --git a/certbot-nginx/certbot_nginx/tests/testdata/etc_nginx/nginx.new.conf b/certbot-nginx/certbot_nginx/tests/testdata/etc_nginx/nginx.new.conf deleted file mode 100644 index 59c1c968f..000000000 --- a/certbot-nginx/certbot_nginx/tests/testdata/etc_nginx/nginx.new.conf +++ /dev/null @@ -1,83 +0,0 @@ -user nobody; -worker_processes 1; -error_log logs/error.log; -error_log logs/error.log notice; -error_log logs/error.log info; -pid logs/nginx.pid; -events { - worker_connections 1024; -} -include foo.conf; -http { - include mime.types; - include sites-enabled/*; - default_type application/octet-stream; - log_format main '$remote_addr - $remote_user [$time_local] "$request" ' - '$status $body_bytes_sent "$http_referer" ' - '"$http_user_agent" "$http_x_forwarded_for"'; - access_log logs/access.log main; - sendfile on; - tcp_nopush on; - keepalive_timeout 0; - gzip on; - - server { - listen 8080; - server_name localhost; - server_name ~^(www\.)?(example|bar)\.; - charset koi8-r; - access_log logs/host.access.log main; - - location / { - root html; - index index.html index.htm; - } - error_page 404 /404.html; - error_page 500 502 503 504 /50x.html; - - location = /50x.html { - root html; - } - - location ~ \.php$ { - proxy_pass http://127.0.0.1; - } - - location ~ \.php$ { - root html; - fastcgi_pass 127.0.0.1:9000; - fastcgi_index index.php; - fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name; - } - - location ~ /\.ht { - deny all; - } - } - - server { - listen 8000; - listen somename:8080; - include server.conf; - - location / { - root html; - index index.html index.htm; - } - } - - server { - listen 443 ssl; - server_name localhost; - ssl_certificate cert.pem; - ssl_certificate_key cert.key; - ssl_session_cache shared:SSL:1m; - ssl_session_timeout 5m; - ssl_ciphers HIGH:!aNULL:!MD5; - - location / { - root html; - index index.html index.htm; - } - } -} From 9459daa6d03940f6c8fdfe169807a32df2a36b14 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Thu, 16 Jun 2016 18:04:13 -0700 Subject: [PATCH 331/396] Delete new.conf files after tests Not least, to prevent git conflicts with old branches --- .../certbot_nginx/tests/nginxparser_test.py | 38 +++++++++++-------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/certbot-nginx/certbot_nginx/tests/nginxparser_test.py b/certbot-nginx/certbot_nginx/tests/nginxparser_test.py index 2efacd940..87c7a0430 100644 --- a/certbot-nginx/certbot_nginx/tests/nginxparser_test.py +++ b/certbot-nginx/certbot_nginx/tests/nginxparser_test.py @@ -136,9 +136,12 @@ class TestRawNginxParser(unittest.TestCase): dump(parsed, handle) with open(util.get_data_filename('nginx.new.conf')) as handle: parsed_new = load(handle) - self.maxDiff = None - self.assertEquals(parsed[0], parsed_new[0]) - self.assertEquals(parsed[1:], parsed_new[1:]) + try: + self.maxDiff = None + self.assertEquals(parsed[0], parsed_new[0]) + self.assertEquals(parsed[1:], parsed_new[1:]) + finally: + os.unlink(util.get_data_filename('nginx.new.conf')) def test_comments(self): with open(util.get_data_filename('minimalistic_comments.conf')) as handle: @@ -150,20 +153,23 @@ class TestRawNginxParser(unittest.TestCase): with open(util.get_data_filename('minimalistic_comments.new.conf')) as handle: parsed_new = load(handle) - self.assertEquals(parsed, parsed_new) + try: + self.assertEquals(parsed, parsed_new) - self.assertEqual(parsed_new, [ - ['#', " Use bar.conf when it's a full moon!"], - ['include', 'foo.conf'], - ['#', ' Kilroy was here'], - ['check_status'], - [['server'], - [['#'], - ['#', " Don't forget to open up your firewall!"], - ['#'], - ['listen', '1234'], - ['#', ' listen 80;']]], - ]) + self.assertEqual(parsed_new, [ + ['#', " Use bar.conf when it's a full moon!"], + ['include', 'foo.conf'], + ['#', ' Kilroy was here'], + ['check_status'], + [['server'], + [['#'], + ['#', " Don't forget to open up your firewall!"], + ['#'], + ['listen', '1234'], + ['#', ' listen 80;']]], + ]) + finally: + os.unlink(util.get_data_filename('minimalistic_comments.new.conf')) def test_issue_518(self): parsed = loads('if ($http_accept ~* "webp") { set $webp "true"; }') From efd1ff46c696294341f9f8537e25023c8d01f196 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Thu, 16 Jun 2016 18:18:33 -0700 Subject: [PATCH 332/396] Lint --- certbot-nginx/certbot_nginx/nginxparser.py | 10 ++++------ certbot-nginx/certbot_nginx/parser.py | 1 - certbot-nginx/certbot_nginx/tests/nginxparser_test.py | 1 + certbot-nginx/certbot_nginx/tls_sni_01.py | 4 +--- 4 files changed, 6 insertions(+), 10 deletions(-) diff --git a/certbot-nginx/certbot_nginx/nginxparser.py b/certbot-nginx/certbot_nginx/nginxparser.py index d7248ad49..1664f94cf 100644 --- a/certbot-nginx/certbot_nginx/nginxparser.py +++ b/certbot-nginx/certbot_nginx/nginxparser.py @@ -1,5 +1,6 @@ """Very low-level nginx config parser based on pyparsing.""" import copy +import logging import string from pyparsing import ( @@ -8,6 +9,7 @@ from pyparsing import ( from pyparsing import stringEnd from pyparsing import restOfLine +logger = logging.getLogger(__name__) class RawNginxParser(object): # pylint: disable=expression-not-assigned @@ -97,11 +99,7 @@ class RawNginxDumper(object): # if so rotate it into gap if values and spacey(values): gap = values - try: - values = b.pop(0) - except: - import ipdb - ipdb.set_trace() + values = b.pop(0) #if values is None: # yield indentation + key + gap + ';' #else: @@ -200,7 +198,7 @@ class UnspacedList(list): self.spaced.extend(x.spaced) else: self.spaced.extend(x) - self.logger.debug("Weird, extending regular list %r to Unspaced %r", x, self) + logger.debug("Weird, extending regular list %r to Unspaced %r", x, self) list.extend(self, x) def __add__(self, other): diff --git a/certbot-nginx/certbot_nginx/parser.py b/certbot-nginx/certbot_nginx/parser.py index 61c719816..21127bc08 100644 --- a/certbot-nginx/certbot_nginx/parser.py +++ b/certbot-nginx/certbot_nginx/parser.py @@ -4,7 +4,6 @@ import logging import os import pyparsing import re -import sys from certbot import errors diff --git a/certbot-nginx/certbot_nginx/tests/nginxparser_test.py b/certbot-nginx/certbot_nginx/tests/nginxparser_test.py index 87c7a0430..7354c118c 100644 --- a/certbot-nginx/certbot_nginx/tests/nginxparser_test.py +++ b/certbot-nginx/certbot_nginx/tests/nginxparser_test.py @@ -1,5 +1,6 @@ """Test for certbot_nginx.nginxparser.""" import operator +import os import unittest from pyparsing import ParseException diff --git a/certbot-nginx/certbot_nginx/tls_sni_01.py b/certbot-nginx/certbot_nginx/tls_sni_01.py index 13ae2c358..e4c5d31a6 100644 --- a/certbot-nginx/certbot_nginx/tls_sni_01.py +++ b/certbot-nginx/certbot_nginx/tls_sni_01.py @@ -3,7 +3,6 @@ import itertools import logging import os -import sys from certbot import errors from certbot.plugins import common @@ -124,8 +123,7 @@ class NginxTlsSni01(common.TLSSNI01): True, self.challenge_conf) with open(self.challenge_conf, "w") as new_conf: - out = nginxparser.dumps(config) - new_conf.write(out) + nginxparser.dump(config, new_conf) def _make_server_block(self, achall, addrs): """Creates a server block for a challenge. From 6666ff25d49deb74c7976dd12a9a384ddde39083 Mon Sep 17 00:00:00 2001 From: Amjad Mashaal Date: Fri, 17 Jun 2016 03:57:49 +0200 Subject: [PATCH 333/396] Moving import dialog to the top of the file --- certbot/tests/cli_test.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/certbot/tests/cli_test.py b/certbot/tests/cli_test.py index 9a2009e43..9c81c070b 100644 --- a/certbot/tests/cli_test.py +++ b/certbot/tests/cli_test.py @@ -2,6 +2,7 @@ from __future__ import print_function import argparse +import dialog import functools import itertools import os @@ -923,8 +924,6 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods traceback.format_exception_only(KeyboardInterrupt, interrupt))) # Test dialog errors - - import dialog exception = dialog.error(message="test message") main._handle_exception( dialog.DialogError, exc_value=exception, trace=None, config=None) From 31d9fc7d865c4567f56b3991c90e915a620aaa2e Mon Sep 17 00:00:00 2001 From: Amjad Mashaal Date: Fri, 17 Jun 2016 04:13:44 +0200 Subject: [PATCH 334/396] Adding --skip-missing-interpreters to constributing.rst "Submitting a pull request" section --- docs/contributing.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/contributing.rst b/docs/contributing.rst index 9ceb0fbdd..633af19c6 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -317,7 +317,9 @@ Steps: 3. Run ``./pep8.travis.sh`` to do a cursory check of your code style. Fix any errors. 4. Run ``tox -e lint`` to check for pylint errors. Fix any errors. -5. Run ``tox`` to run the entire test suite including coverage. Fix any errors. +5. Run ``tox --skip-missing-interpreters`` to run the entire test suite + including coverage. The ``--skip-missing-interpreters`` argument ignores + missing versions of Python needed for running the tests. Fix any errors. 6. If your code touches communication with an ACME server/Boulder, you should run the integration tests, see `integration`_. See `Known Issues`_ for some common failures that have nothing to do with your code. From f5282ed2de63342d09e593cb1a9338383ab2b9b1 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Fri, 17 Jun 2016 10:48:54 -0700 Subject: [PATCH 335/396] Show lines missing test coverage in test output --- .coveragerc | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .coveragerc diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 000000000..087900105 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,3 @@ +[report] +# show lines missing coverage in output +show_missing = True From 340e4dc0f27f5a2db86e2e639f5392723bf1204e Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Fri, 17 Jun 2016 10:50:02 -0700 Subject: [PATCH 336/396] Remove unused nosexcover dependency --- setup.py | 1 - 1 file changed, 1 deletion(-) diff --git a/setup.py b/setup.py index 53fde5282..21cda901a 100644 --- a/setup.py +++ b/setup.py @@ -69,7 +69,6 @@ dev_extras = [ 'astroid==1.3.5', 'coverage', 'nose', - 'nosexcover', 'pep8', 'pylint==1.4.2', # upstream #248 'tox', From ba0a0e9c2664ffade3bc916cf5fc10d8c5d57a53 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Fri, 17 Jun 2016 14:32:24 -0700 Subject: [PATCH 337/396] Tests for UnspacedList --- certbot-nginx/certbot_nginx/nginxparser.py | 20 +++---- .../certbot_nginx/tests/nginxparser_test.py | 54 +++++++++++++++++++ 2 files changed, 65 insertions(+), 9 deletions(-) diff --git a/certbot-nginx/certbot_nginx/nginxparser.py b/certbot-nginx/certbot_nginx/nginxparser.py index 1664f94cf..caf6fe725 100644 --- a/certbot-nginx/certbot_nginx/nginxparser.py +++ b/certbot-nginx/certbot_nginx/nginxparser.py @@ -164,16 +164,15 @@ spacey = lambda x: (isinstance(x, str) and x.isspace()) or x == '' class UnspacedList(list): """Wrap a list [of lists], making any whitespace entries magically invisible""" - def __init__(self, list_source, top=False): + def __init__(self, list_source): self.spaced = copy.deepcopy(list(list_source)) # Turn self into a version of the source list that has spaces removed # and all sub-lists also UnspacedList()ed list.__init__(self, list_source) - self.top = top if top else self for i, entry in reversed(list(enumerate(self))): if isinstance(entry, list): - sublist = UnspacedList(entry, top=self.top) + sublist = UnspacedList(entry) list.__setitem__(self, i, sublist) self.spaced[i] = sublist.spaced elif spacey(entry): @@ -202,12 +201,9 @@ class UnspacedList(list): list.extend(self, x) def __add__(self, other): - if hasattr(other, "spaced"): - # If the thing added to us is an UnspacedList, use its spaced form - self.spaced.__add__(other.spaced) - else: - self.spaced.__add__(other) - list.__add__(self, other) + l = copy.deepcopy(self) + l.extend(other) + return l def __setitem__(self, i, value): if hasattr(value, "spaced"): @@ -220,6 +216,12 @@ class UnspacedList(list): self.spaced.__delitem__(i + self._spaces_before(i)) list.__delitem__(self, i) + def __deepcopy__(self, memo): + l = UnspacedList(self[:]) + l.spaced = copy.deepcopy(self.spaced) + return l + + def _spaces_before(self, idx): "Count the number of spaces in the spaced list before pos idx in the spaceless one" spaces = 0 diff --git a/certbot-nginx/certbot_nginx/tests/nginxparser_test.py b/certbot-nginx/certbot_nginx/tests/nginxparser_test.py index 7354c118c..e78adfac1 100644 --- a/certbot-nginx/certbot_nginx/tests/nginxparser_test.py +++ b/certbot-nginx/certbot_nginx/tests/nginxparser_test.py @@ -1,4 +1,5 @@ """Test for certbot_nginx.nginxparser.""" +import copy import operator import os import unittest @@ -180,5 +181,58 @@ class TestRawNginxParser(unittest.TestCase): [['set', '$webp "true"']]] ]) +class TestUnspacedList(unittest.TestCase): + """Test the raw low-level Nginx config parser.""" + def setUp(self): + self.a = ["\n ", "things", " ", "quirk"] + self.b = ["y", " "] + self.l = self.a[:] + self.l2 = self.b[:] + self.ul = UnspacedList(self.l) + self.ul2 = UnspacedList(self.l2) + + def test_construction(self): + self.assertEqual(self.ul, ["things", "quirk"]) + self.assertEqual(self.ul2, ["y"]) + + def test_append(self): + ul3 = copy.deepcopy(self.ul) + ul3.append("wise") + self.assertEqual(ul3, ["things", "quirk", "wise"]) + self.assertEqual(ul3.spaced, self.a + ["wise"]) + + def test_add(self): + ul3 = self.ul + self.ul2 + self.assertEqual(ul3, ["things", "quirk", "y"]) + self.assertEqual(ul3.spaced, self.a + self.b) + self.assertEqual(self.ul.spaced, self.a) + ul3 = self.ul + self.l2 + self.assertEqual(ul3, ["things", "quirk", "y", " "]) + self.assertEqual(ul3.spaced, self.a + self.b) + + def test_extend(self): + ul3 = copy.deepcopy(self.ul) + ul3.extend(self.ul2) + self.assertEqual(ul3, ["things", "quirk", "y"]) + self.assertEqual(ul3.spaced, self.a + self.b) + self.assertEqual(self.ul.spaced, self.a) + + def test_set(self): + ul3 = copy.deepcopy(self.ul) + ul3[0] = "zither" + l = ["\n ", "zather", "zest"] + ul3[1] = UnspacedList(l) + self.assertEqual(ul3, ["zither", ["zather", "zest"]]) + self.assertEqual(ul3.spaced, [self.a[0], "zither", " ", l]) + + def test_rawlists(self): + ul3 = copy.deepcopy(self.ul) + ul3.insert(0, "some") + ul3.append("why") + ul3.extend(["did", "whether"]) + del ul3[2] + self.assertEqual(ul3, ["some", "things", "why", "did", "whether"]) + + if __name__ == '__main__': unittest.main() # pragma: no cover From b2c36f85274176a31ed16bfe7b210793165d3fac Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Fri, 17 Jun 2016 14:39:55 -0700 Subject: [PATCH 338/396] Lint & test fix --- certbot-nginx/certbot_nginx/nginxparser.py | 2 +- certbot-nginx/certbot_nginx/tests/parser_test.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/certbot-nginx/certbot_nginx/nginxparser.py b/certbot-nginx/certbot_nginx/nginxparser.py index caf6fe725..765194806 100644 --- a/certbot-nginx/certbot_nginx/nginxparser.py +++ b/certbot-nginx/certbot_nginx/nginxparser.py @@ -216,7 +216,7 @@ class UnspacedList(list): self.spaced.__delitem__(i + self._spaces_before(i)) list.__delitem__(self, i) - def __deepcopy__(self, memo): + def __deepcopy__(self, unused_memo): l = UnspacedList(self[:]) l.spaced = copy.deepcopy(self.spaced) return l diff --git a/certbot-nginx/certbot_nginx/tests/parser_test.py b/certbot-nginx/certbot_nginx/tests/parser_test.py index 8ac995dfc..6d046178a 100644 --- a/certbot-nginx/certbot_nginx/tests/parser_test.py +++ b/certbot-nginx/certbot_nginx/tests/parser_test.py @@ -126,7 +126,7 @@ class NginxParserTest(util.NginxTest): nparser.add_server_directives(nparser.abs_path('nginx.conf'), set(['localhost', r'~^(www\.)?(example|bar)\.']), - [['foo', 'bar'], ['ssl_certificate', + [['foo', 'bar'], ['\n ', 'ssl_certificate', ' ', '/etc/ssl/cert.pem']], replace=False) ssl_re = re.compile(r'\n\s+ssl_certificate /etc/ssl/cert.pem') From 6156b452bcf7c924f12c334e7094ff5a9049ee37 Mon Sep 17 00:00:00 2001 From: Blake Griffith Date: Tue, 24 May 2016 14:04:55 -0500 Subject: [PATCH 339/396] Fix FQDN checks. --- certbot/util.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/certbot/util.py b/certbot/util.py index 35c599737..9739e8d2f 100644 --- a/certbot/util.py +++ b/certbot/util.py @@ -423,14 +423,17 @@ def enforce_domain_sanity(domain): # It wasn't an IP address, so that's good pass - # FQDN checks from - # http://www.mkyong.com/regular-expressions/domain-name-regular-expression-example/ - # Characters used, domain parts < 63 chars, tld > 1 < 64 chars - # first and last char is not "-" - fqdn = re.compile("^((?!-)[A-Za-z0-9-]{1,63}(? 256: + raise errors.ConfigurationError(msg + "it is too long.") + return domain From a7a2049d69debf457cec637ddf4e5c9ca9848c9f Mon Sep 17 00:00:00 2001 From: Blake Griffith Date: Tue, 24 May 2016 14:14:44 -0500 Subject: [PATCH 340/396] Fix FQDN tests. --- certbot/tests/cli_test.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/certbot/tests/cli_test.py b/certbot/tests/cli_test.py index 9c81c070b..896550837 100644 --- a/certbot/tests/cli_test.py +++ b/certbot/tests/cli_test.py @@ -342,11 +342,11 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods # FQDN self.assertRaises(errors.ConfigurationError, self._call, - ['-d', 'comma,gotwrong.tld']) + ['-d', 'a' * 64]) # FQDN 2 self.assertRaises(errors.ConfigurationError, self._call, - ['-d', 'illegal.character=.tld']) + ['-d', (('a' * 50) + '.') * 10]) # Wildcard self.assertRaises(errors.ConfigurationError, self._call, From 2625daad75a9233ee0f2d57576adff23e6db1779 Mon Sep 17 00:00:00 2001 From: Blake Griffith Date: Tue, 24 May 2016 14:25:29 -0500 Subject: [PATCH 341/396] Fix more FQDN tests in ops_test.py --- certbot/tests/display/ops_test.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/certbot/tests/display/ops_test.py b/certbot/tests/display/ops_test.py index 3aff37d86..26f67b69f 100644 --- a/certbot/tests/display/ops_test.py +++ b/certbot/tests/display/ops_test.py @@ -248,9 +248,9 @@ class ChooseNamesTest(unittest.TestCase): def test_get_valid_domains(self): from certbot.display.ops import get_valid_domains all_valid = ["example.com", "second.example.com", - "also.example.com"] - all_invalid = ["xn--ls8h.tld", "*.wildcard.com", "notFQDN", - "uniçodé.com"] + "also.example.com", "under_score.example.com", + "justtld"] + all_invalid = ["xn--ls8h.tld", "*.wildcard.com", "uniçodé.com"] two_valid = ["example.com", "xn--ls8h.tld", "also.example.com"] self.assertEqual(get_valid_domains(all_valid), all_valid) self.assertEqual(get_valid_domains(all_invalid), []) @@ -276,19 +276,18 @@ class ChooseNamesTest(unittest.TestCase): mock_util().input.return_value = (display_util.OK, "xn--ls8h.tld") self.assertEqual(_choose_names_manually(), []) - # non-FQDN and no retry - mock_util().input.return_value = (display_util.OK, - "notFQDN") - self.assertEqual(_choose_names_manually(), []) - # Two valid domains + # Valid domains mock_util().input.return_value = (display_util.OK, ("example.com," + "under_score.example.com," + "justtld," "valid.example.com")) self.assertEqual(_choose_names_manually(), - ["example.com", "valid.example.com"]) + ["example.com", "under_score.example.com", + "justtld", "valid.example.com"]) # Three iterations mock_util().input.return_value = (display_util.OK, - "notFQDN") + "uniçodé.com") yn = mock.MagicMock() yn.side_effect = [True, True, False] mock_util().yesno = yn From a148d2ddfa60206f1a648dd7c9851e5921ea1805 Mon Sep 17 00:00:00 2001 From: Blake Griffith Date: Fri, 17 Jun 2016 18:58:48 -0500 Subject: [PATCH 342/396] Limit domains to 255 octets. --- certbot/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certbot/util.py b/certbot/util.py index 9739e8d2f..301fc669b 100644 --- a/certbot/util.py +++ b/certbot/util.py @@ -431,7 +431,7 @@ def enforce_domain_sanity(domain): for l in labels: if not 0 < len(l) < 64: raise errors.ConfigurationError(msg + "label {0} is too long.".format(l)) - if len(domain) > 256: + if len(domain) > 255: raise errors.ConfigurationError(msg + "it is too long.") return domain From 679101cfb0659eba484a625bc244a595d86edf93 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Fri, 17 Jun 2016 18:29:45 -0700 Subject: [PATCH 343/396] Object printing improvements --- certbot-nginx/certbot_nginx/obj.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/certbot-nginx/certbot_nginx/obj.py b/certbot-nginx/certbot_nginx/obj.py index 0d1151f39..20f9f0a6f 100644 --- a/certbot-nginx/certbot_nginx/obj.py +++ b/certbot-nginx/certbot_nginx/obj.py @@ -85,6 +85,9 @@ class Addr(common.Addr): return parts + def __repr__(self): + return "Addr(" + self.__str__() + ")" + def __eq__(self, other): if isinstance(other, self.__class__): return (self.tup == other.tup and @@ -126,6 +129,9 @@ class VirtualHost(object): # pylint: disable=too-few-public-methods "enabled: %s" % (self.filep, addr_str, self.names, self.ssl, self.enabled)) + def __repr__(self): + return "VirtualHost(" + self.__str__().replace("\n",",") + ")\n" + def __eq__(self, other): if isinstance(other, self.__class__): return (self.filep == other.filep and From 7bcc23d9f54fea226538dc3d6d4e7f2d37232977 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Fri, 17 Jun 2016 18:30:12 -0700 Subject: [PATCH 344/396] Debugging --- certbot-nginx/certbot_nginx/tests/tls_sni_01_test.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/certbot-nginx/certbot_nginx/tests/tls_sni_01_test.py b/certbot-nginx/certbot_nginx/tests/tls_sni_01_test.py index 3264d6ed3..19d22365d 100644 --- a/certbot-nginx/certbot_nginx/tests/tls_sni_01_test.py +++ b/certbot-nginx/certbot_nginx/tests/tls_sni_01_test.py @@ -12,6 +12,7 @@ from certbot import errors from certbot.plugins import common_test from certbot.tests import acme_util +from certbot_nginx import nginxparser from certbot_nginx import obj from certbot_nginx.tests import util @@ -132,16 +133,21 @@ class TlsSniPerformTest(util.NginxTest): http = self.sni.configurator.parser.parsed[ self.sni.configurator.parser.loc["root"]][-1] + print "http", http + #print "SPACED\n", http.spaced self.assertTrue(['include', self.sni.challenge_conf] in http[1]) vhosts = self.sni.configurator.parser.get_vhosts() + print "Got", vhosts vhs = [vh for vh in vhosts if vh.filep == self.sni.challenge_conf] + print "And now", vhs for vhost in vhs: if vhost.addrs == set(v_addr1): response = self.achalls[0].response(self.account_key) else: response = self.achalls[2].response(self.account_key) + print vhost.addrs, set(v_addr2) self.assertEqual(vhost.addrs, set(v_addr2)) self.assertEqual(vhost.names, set([response.z_domain])) From 3c9f4d5fc73204ce9aa840e4eabd8b3dbd01cf74 Mon Sep 17 00:00:00 2001 From: Joona Hoikkala Date: Sat, 18 Jun 2016 23:50:30 +0300 Subject: [PATCH 345/396] If port is set for any IP, do not attempt to autoconfigure --- certbot-apache/certbot_apache/configurator.py | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/certbot-apache/certbot_apache/configurator.py b/certbot-apache/certbot_apache/configurator.py index e4c06ba7e..392670985 100644 --- a/certbot-apache/certbot_apache/configurator.py +++ b/certbot-apache/certbot_apache/configurator.py @@ -624,11 +624,15 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): # Note: This could be made to also look for ip:443 combo listens = [self.parser.get_arg(x).split()[0] for x in self.parser.find_dir("Listen")] + # In case no Listens are set (which really is a broken apache config) if not listens: listens = ["80"] - if port in listens: + + # Listen already in place + if self._has_port_already(listens, port): return + for listen in listens: # For any listen statement, check if the machine also listens on # Port 443. If not, add such a listen statement. @@ -664,6 +668,23 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): self.parser.loc["listen"]) listens.append("%s:%s" % (ip, port)) + def _has_port_already(self, listens, port): + """Helper method for prepare_server_https to find out if user + already has an active Listen statement for the port we need + + :param list listens: List of listen variables + :param string port: Port in question + """ + + if port in listens: + return True + # Check if Apache is already listening on a specific IP + for listen in listens: + if len(listen.split(":")) > 1: + # Ugly but takes care of protocol def, eg: 1.1.1.1:443 https + if listen.split(":")[-1].split(" ")[0] == port: + return True + def prepare_https_modules(self, temp): """Helper method for prepare_server_https, taking care of enabling needed modules From e4f88506cc84b852b6bc339cede9ed8abfebc2db Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Sat, 18 Jun 2016 14:52:07 -0700 Subject: [PATCH 346/396] Fix TLS_SNI & associated tests --- certbot-nginx/certbot_nginx/nginxparser.py | 20 +++++++++++++----- certbot-nginx/certbot_nginx/obj.py | 2 +- certbot-nginx/certbot_nginx/parser.py | 7 +++++-- .../certbot_nginx/tests/configurator_test.py | 2 +- .../certbot_nginx/tests/nginxparser_test.py | 4 ++-- .../certbot_nginx/tests/tls_sni_01_test.py | 5 ----- certbot-nginx/certbot_nginx/tests/util.py | 11 +++++++--- certbot-nginx/certbot_nginx/tls_sni_01.py | 21 ++++++++++--------- 8 files changed, 43 insertions(+), 29 deletions(-) diff --git a/certbot-nginx/certbot_nginx/nginxparser.py b/certbot-nginx/certbot_nginx/nginxparser.py index 765194806..6b3896fd9 100644 --- a/certbot-nginx/certbot_nginx/nginxparser.py +++ b/certbot-nginx/certbot_nginx/nginxparser.py @@ -165,6 +165,7 @@ class UnspacedList(list): """Wrap a list [of lists], making any whitespace entries magically invisible""" def __init__(self, list_source): + # ensure our argument is not a generator, and duplicate any sublists self.spaced = copy.deepcopy(list(list_source)) # Turn self into a version of the source list that has spaces removed @@ -173,16 +174,25 @@ class UnspacedList(list): for i, entry in reversed(list(enumerate(self))): if isinstance(entry, list): sublist = UnspacedList(entry) - list.__setitem__(self, i, sublist) + if sublist != [] or sublist.spaced == []: + list.__setitem__(self, i, sublist) + else: + # if a sublist is exclusively spacey entries, it might + # choke the high level parser, so make it disappear + list.__delitem__(self, i) self.spaced[i] = sublist.spaced elif spacey(entry): - list.__delitem__(self, i) + # don't delete comments + if "#" not in self[:i]: + list.__delitem__(self, i) def insert(self, i, x): - if hasattr(x, "spaced"): - self.spaced.insert(i + self._spaces_before(i), x.spaced) - else: + if not isinstance(x, list): # str or None self.spaced.insert(i + self._spaces_before(i), x) + else: + if not hasattr(x, "spaced"): + x = UnspacedList(x) + self.spaced.insert(i + self._spaces_before(i), x.spaced) list.insert(self, i, x) def append(self, x): diff --git a/certbot-nginx/certbot_nginx/obj.py b/certbot-nginx/certbot_nginx/obj.py index 20f9f0a6f..a559b5e02 100644 --- a/certbot-nginx/certbot_nginx/obj.py +++ b/certbot-nginx/certbot_nginx/obj.py @@ -130,7 +130,7 @@ class VirtualHost(object): # pylint: disable=too-few-public-methods self.names, self.ssl, self.enabled)) def __repr__(self): - return "VirtualHost(" + self.__str__().replace("\n",",") + ")\n" + return "VirtualHost(" + self.__str__().replace("\n",", ") + ")\n" def __eq__(self, other): if isinstance(other, self.__class__): diff --git a/certbot-nginx/certbot_nginx/parser.py b/certbot-nginx/certbot_nginx/parser.py index 21127bc08..31595d56d 100644 --- a/certbot-nginx/certbot_nginx/parser.py +++ b/certbot-nginx/certbot_nginx/parser.py @@ -1,4 +1,5 @@ """NginxParser is a member object of the NginxConfigurator class.""" +import copy import glob import logging import os @@ -113,6 +114,7 @@ class NginxParser(object): for filename in servers: for server in servers[filename]: # Parse the server block into a VirtualHost object + parsed_server = parse_server(server) vhost = obj.VirtualHost(filename, parsed_server['addrs'], @@ -132,7 +134,7 @@ class NginxParser(object): :rtype: list """ - result = list(block) # Copy the list to keep self.parsed idempotent + result = copy.deepcopy(block) # Copy the list to keep self.parsed idempotent for directive in block: if _is_include_directive(directive): included_files = glob.glob( @@ -465,6 +467,8 @@ def parse_server(server): 'names': set()} for directive in server: + if not directive: + continue if directive[0] == 'listen': addr = obj.Addr.fromstring(directive[1]) parsed_server['addrs'].add(addr) @@ -506,7 +510,6 @@ def _add_directive(block, directive, replace): """ directive = nginxparser.UnspacedList(directive) - print "Unspacified", directive.spaced, directive if len(directive) == 0: # whitespace block.append(directive) diff --git a/certbot-nginx/certbot_nginx/tests/configurator_test.py b/certbot-nginx/certbot_nginx/tests/configurator_test.py index 30f287249..a1d0e75cc 100644 --- a/certbot-nginx/certbot_nginx/tests/configurator_test.py +++ b/certbot-nginx/certbot_nginx/tests/configurator_test.py @@ -83,7 +83,7 @@ class NginxConfiguratorTest(util.NginxTest): filep = self.config.parser.abs_path('sites-enabled/example.com') self.config.parser.add_server_directives( filep, set(['.example.com', 'example.*']), - [['listen', '5001 ssl']], + [['listen', ' ', '5001 ssl']], replace=False) self.config.save() diff --git a/certbot-nginx/certbot_nginx/tests/nginxparser_test.py b/certbot-nginx/certbot_nginx/tests/nginxparser_test.py index e78adfac1..2e8011443 100644 --- a/certbot-nginx/certbot_nginx/tests/nginxparser_test.py +++ b/certbot-nginx/certbot_nginx/tests/nginxparser_test.py @@ -164,9 +164,9 @@ class TestRawNginxParser(unittest.TestCase): ['#', ' Kilroy was here'], ['check_status'], [['server'], - [['#'], + [['#', ''], ['#', " Don't forget to open up your firewall!"], - ['#'], + ['#', ''], ['listen', '1234'], ['#', ' listen 80;']]], ]) diff --git a/certbot-nginx/certbot_nginx/tests/tls_sni_01_test.py b/certbot-nginx/certbot_nginx/tests/tls_sni_01_test.py index 19d22365d..51301145c 100644 --- a/certbot-nginx/certbot_nginx/tests/tls_sni_01_test.py +++ b/certbot-nginx/certbot_nginx/tests/tls_sni_01_test.py @@ -133,21 +133,16 @@ class TlsSniPerformTest(util.NginxTest): http = self.sni.configurator.parser.parsed[ self.sni.configurator.parser.loc["root"]][-1] - print "http", http - #print "SPACED\n", http.spaced self.assertTrue(['include', self.sni.challenge_conf] in http[1]) vhosts = self.sni.configurator.parser.get_vhosts() - print "Got", vhosts vhs = [vh for vh in vhosts if vh.filep == self.sni.challenge_conf] - print "And now", vhs for vhost in vhs: if vhost.addrs == set(v_addr1): response = self.achalls[0].response(self.account_key) else: response = self.achalls[2].response(self.account_key) - print vhost.addrs, set(v_addr2) self.assertEqual(vhost.addrs, set(v_addr2)) self.assertEqual(vhost.names, set([response.z_domain])) diff --git a/certbot-nginx/certbot_nginx/tests/util.py b/certbot-nginx/certbot_nginx/tests/util.py index ddacd041b..4ea4a03a7 100644 --- a/certbot-nginx/certbot_nginx/tests/util.py +++ b/certbot-nginx/certbot_nginx/tests/util.py @@ -1,4 +1,5 @@ """Common utilities for certbot_nginx.""" +import copy import os import pkg_resources import unittest @@ -16,6 +17,7 @@ from certbot.plugins import common from certbot_nginx import constants from certbot_nginx import configurator +from certbot_nginx import nginxparser class NginxTest(unittest.TestCase): # pylint: disable=too-few-public-methods @@ -82,12 +84,15 @@ def filter_comments(tree): def traverse(tree): """Generator dropping comment nodes""" - for key, values in tree: + for entry in tree: + key, values = entry if isinstance(key, list): - yield [key, filter_comments(values)] + new = copy.deepcopy(entry) + new[1] = filter_comments(values) + yield new else: if key != '#': - yield [key, values] + yield entry return list(traverse(tree)) diff --git a/certbot-nginx/certbot_nginx/tls_sni_01.py b/certbot-nginx/certbot_nginx/tls_sni_01.py index e4c5d31a6..a9f31b84a 100644 --- a/certbot-nginx/certbot_nginx/tls_sni_01.py +++ b/certbot-nginx/certbot_nginx/tls_sni_01.py @@ -93,10 +93,10 @@ class NginxTlsSni01(common.TLSSNI01): # Add the 'include' statement for the challenges if it doesn't exist # already in the main config included = False - include_directive = ['include', self.challenge_conf] + include_directive = ['include', ' ', self.challenge_conf] root = self.configurator.parser.loc["root"] - bucket_directive = ['server_names_hash_bucket_size', '128'] + bucket_directive = ['server_names_hash_bucket_size', ' ', '128'] main = self.configurator.parser.parsed[root] for key, body in main: @@ -118,6 +118,7 @@ class NginxTlsSni01(common.TLSSNI01): config = [self._make_server_block(pair[0], pair[1]) for pair in itertools.izip(self.achalls, ll_addrs)] + config = nginxparser.UnspacedList(config) self.configurator.reverter.register_file_creation( True, self.challenge_conf) @@ -142,19 +143,19 @@ class NginxTlsSni01(common.TLSSNI01): document_root = os.path.join( self.configurator.config.work_dir, "tls_sni_01_page") - block = [['listen', str(addr)] for addr in addrs] + block = [['listen', ' ', str(addr)] for addr in addrs] - block.extend([['server_name', + block.extend([['server_name', ' ', achall.response(achall.account_key).z_domain], - ['include', self.configurator.parser.loc["ssl_options"]], + ['include', ' ', self.configurator.parser.loc["ssl_options"]], # access and error logs necessary for # integration testing (non-root) - ['access_log', os.path.join( + ['access_log', ' ', os.path.join( self.configurator.config.work_dir, 'access.log')], - ['error_log', os.path.join( + ['error_log', ' ', os.path.join( self.configurator.config.work_dir, 'error.log')], - ['ssl_certificate', self.get_cert_path(achall)], - ['ssl_certificate_key', self.get_key_path(achall)], - [['location', '/'], [['root', document_root]]]]) + ['ssl_certificate', ' ', self.get_cert_path(achall)], + ['ssl_certificate_key', ' ', self.get_key_path(achall)], + [['location', ' ', '/'], [['root', ' ', document_root]]]]) return [['server'], block] From 1e59bf81123df94210f5a4627e1af7e444b1eff2 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Sat, 18 Jun 2016 14:53:17 -0700 Subject: [PATCH 347/396] fixup --- certbot-nginx/certbot_nginx/tests/tls_sni_01_test.py | 1 - 1 file changed, 1 deletion(-) diff --git a/certbot-nginx/certbot_nginx/tests/tls_sni_01_test.py b/certbot-nginx/certbot_nginx/tests/tls_sni_01_test.py index 51301145c..3264d6ed3 100644 --- a/certbot-nginx/certbot_nginx/tests/tls_sni_01_test.py +++ b/certbot-nginx/certbot_nginx/tests/tls_sni_01_test.py @@ -12,7 +12,6 @@ from certbot import errors from certbot.plugins import common_test from certbot.tests import acme_util -from certbot_nginx import nginxparser from certbot_nginx import obj from certbot_nginx.tests import util From b663ec039ed41012f89d2dcac950be323a7d3a19 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Sat, 18 Jun 2016 15:05:48 -0700 Subject: [PATCH 348/396] lint --- certbot-nginx/certbot_nginx/obj.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certbot-nginx/certbot_nginx/obj.py b/certbot-nginx/certbot_nginx/obj.py index a559b5e02..f5ac88f6c 100644 --- a/certbot-nginx/certbot_nginx/obj.py +++ b/certbot-nginx/certbot_nginx/obj.py @@ -130,7 +130,7 @@ class VirtualHost(object): # pylint: disable=too-few-public-methods self.names, self.ssl, self.enabled)) def __repr__(self): - return "VirtualHost(" + self.__str__().replace("\n",", ") + ")\n" + return "VirtualHost(" + self.__str__().replace("\n", ", ") + ")\n" def __eq__(self, other): if isinstance(other, self.__class__): From d2b4ae57404486f5d5b34aeaea6fc3637c263a75 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Sat, 18 Jun 2016 15:17:59 -0700 Subject: [PATCH 349/396] Consistently coerce inbound data to Unspacines --- certbot-nginx/certbot_nginx/nginxparser.py | 48 ++++++++++++---------- 1 file changed, 27 insertions(+), 21 deletions(-) diff --git a/certbot-nginx/certbot_nginx/nginxparser.py b/certbot-nginx/certbot_nginx/nginxparser.py index 6b3896fd9..d0f53e1a8 100644 --- a/certbot-nginx/certbot_nginx/nginxparser.py +++ b/certbot-nginx/certbot_nginx/nginxparser.py @@ -186,29 +186,37 @@ class UnspacedList(list): if "#" not in self[:i]: list.__delitem__(self, i) - def insert(self, i, x): - if not isinstance(x, list): # str or None - self.spaced.insert(i + self._spaces_before(i), x) + def _coerce(self, inbound): + """ + Coerce some inbound object to be appropriately usable in this object + + :param inbound: string or None or list or UnspacedList + :returns: (coerced UnspacedList or string or None, spaced equivalent) + :rtype: tuple + + """ + if not isinstance(inbound, list): # str or None + return (inbound, inbound) else: if not hasattr(x, "spaced"): - x = UnspacedList(x) - self.spaced.insert(i + self._spaces_before(i), x.spaced) - list.insert(self, i, x) + inbound = UnspacedList(inbound) + return (inbound, inbound.spaced) + + + def insert(self, i, x): + item, spaced_item = self._coerce(x) + self.spaced.insert(i + self._spaces_before(i), spaced_item) + list.insert(self, i, item) def append(self, x): - if hasattr(x, "spaced"): - self.spaced.append(x.spaced) - else: - self.spaced.append(x) - list.append(self, x) + item, spaced_item = self._coerce(x) + self.spaced.append(spaced_item) + list.append(self, item) def extend(self, x): - if hasattr(x, "spaced"): - self.spaced.extend(x.spaced) - else: - self.spaced.extend(x) - logger.debug("Weird, extending regular list %r to Unspaced %r", x, self) - list.extend(self, x) + item, spaced_item = self._coerce(x) + self.spaced.extend(spaced_item) + list.extend(self, item) def __add__(self, other): l = copy.deepcopy(self) @@ -216,10 +224,8 @@ class UnspacedList(list): return l def __setitem__(self, i, value): - if hasattr(value, "spaced"): - self.spaced.__setitem__(i + self._spaces_before(i), value.spaced) - else: - self.spaced.__setitem__(i + self._spaces_before(i), value) + item, spaced_item = self._coerce(x) + self.spaced.__setitem__(i + self._spaces_before(i), value) list.__setitem__(self, i, value) def __delitem__(self, i): From 5a872b829dfb774dd66d8a769c767534a183aae5 Mon Sep 17 00:00:00 2001 From: Joona Hoikkala Date: Mon, 20 Jun 2016 08:57:51 +0300 Subject: [PATCH 350/396] Added tests --- .../certbot_apache/tests/configurator_test.py | 31 ++++++++++++++----- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/certbot-apache/certbot_apache/tests/configurator_test.py b/certbot-apache/certbot_apache/tests/configurator_test.py index e5c09fd1d..9a034c3e0 100644 --- a/certbot-apache/certbot_apache/tests/configurator_test.py +++ b/certbot-apache/certbot_apache/tests/configurator_test.py @@ -497,13 +497,8 @@ class MultipleVhostsTest(util.ApacheTest): # Test Listen statements with specific ip listeed self.config.prepare_server_https("443") - # Should only be 2 here, as the third interface - # already listens to the correct port - self.assertEqual(mock_add_dir.call_count, 2) - - # Check argument to new Listen statements - self.assertEqual(mock_add_dir.call_args_list[0][0][2], ["1.2.3.4:443"]) - self.assertEqual(mock_add_dir.call_args_list[1][0][2], ["[::1]:443"]) + # Should be 0 as one interface already listens to 443 + self.assertEqual(mock_add_dir.call_count, 0) # Reset return lists and inputs mock_add_dir.reset_mock() @@ -519,6 +514,28 @@ class MultipleVhostsTest(util.ApacheTest): self.assertEqual(mock_add_dir.call_args_list[2][0][2], ["1.1.1.1:8080", "https"]) + # mock_get.side_effect = ["1.2.3.4:80", "[::1]:80"] + # mock_find.return_value = ["test1", "test2", "test3"] + # self.config.parser.get_arg = mock_get + # self.config.prepare_server_https("8080", temp=True) + # self.assertEqual(self.listens, 0) + + def test_prepare_server_https_needed_listen(self): + mock_find = mock.Mock() + mock_find.return_value = ["test1", "test2"] + mock_get = mock.Mock() + mock_get.side_effect = ["1.2.3.4:8080", "80"] + mock_add_dir = mock.Mock() + mock_enable = mock.Mock() + + self.config.parser.find_dir = mock_find + self.config.parser.get_arg = mock_get + self.config.parser.add_dir_to_ifmodssl = mock_add_dir + self.config.enable_mod = mock_enable + + self.config.prepare_server_https("443") + self.assertEqual(mock_add_dir.call_count, 1) + def test_prepare_server_https_mixed_listen(self): mock_find = mock.Mock() From 418a5d501f8d18fc3b2dbfc48bb2643aab1dd728 Mon Sep 17 00:00:00 2001 From: Joona Hoikkala Date: Mon, 20 Jun 2016 08:58:22 +0300 Subject: [PATCH 351/396] Refactored adding of listen statements --- certbot-apache/certbot_apache/configurator.py | 72 ++++++++++++------- 1 file changed, 47 insertions(+), 25 deletions(-) diff --git a/certbot-apache/certbot_apache/configurator.py b/certbot-apache/certbot_apache/configurator.py index 2a7064190..9e404177a 100644 --- a/certbot-apache/certbot_apache/configurator.py +++ b/certbot-apache/certbot_apache/configurator.py @@ -625,6 +625,12 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): """ + # If nonstandard port, add service definition for matching + if port != "443": + port_service = "%s %s" % (port, "https") + else: + port_service = port + self.prepare_https_modules(temp) # Check for Listen # Note: This could be made to also look for ip:443 combo @@ -639,40 +645,56 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): if self._has_port_already(listens, port): return + listen_dirs = set(listens) + for listen in listens: # For any listen statement, check if the machine also listens on # Port 443. If not, add such a listen statement. if len(listen.split(":")) == 1: # Its listening to all interfaces - if port not in listens: - if port == "443": - args = [port] - else: - # Non-standard ports should specify https protocol - args = [port, "https"] - self.parser.add_dir_to_ifmodssl( - parser.get_aug_path( - self.parser.loc["listen"]), "Listen", args) - self.save_notes += "Added Listen %s directive to %s\n" % ( - port, self.parser.loc["listen"]) - listens.append(port) + if port not in listen_dirs and port_service not in listen_dirs: + listen_dirs.add(port_service) else: # The Listen statement specifies an ip _, ip = listen[::-1].split(":", 1) ip = ip[::-1] - if "%s:%s" % (ip, port) not in listens: - if port == "443": - args = ["%s:%s" % (ip, port)] - else: - # Non-standard ports should specify https protocol - args = ["%s:%s" % (ip, port), "https"] - self.parser.add_dir_to_ifmodssl( - parser.get_aug_path( - self.parser.loc["listen"]), "Listen", args) - self.save_notes += ("Added Listen %s:%s directive to " - "%s\n") % (ip, port, - self.parser.loc["listen"]) - listens.append("%s:%s" % (ip, port)) + if "%s:%s" % (ip, port_service) not in listen_dirs and ( + "%s:%s" % (ip, port_service) not in listen_dirs): + listen_dirs.add("%s:%s" % (ip, port_service)) + self._add_listens(listen_dirs, listens, port) + + def _add_listens(self, listens, listens_orig, port): + """Helper method for prepare_server_https to figure out which new + listen statements need adding + + :param set listens: Set of all needed Listen statements + :param list listens_orig: List of existing listen statements + :param string port: Port number we're adding + """ + + # Add service definition for non-standard ports + if port != "443": + port_service = "%s %s" % (port, "https") + else: + port_service = port + + new_listens = listens.difference(listens_orig) + + if port in new_listens or port_service in new_listens: + # We have wildcard, skip the rest + self.parser.add_dir_to_ifmodssl( + parser.get_aug_path(self.parser.loc["listen"]), + "Listen", port_service.split(" ")) + self.save_notes += "Added Listen %s directive to %s\n" % ( + port_service, self.parser.loc["listen"]) + else: + for listen in new_listens: + self.parser.add_dir_to_ifmodssl( + parser.get_aug_path(self.parser.loc["listen"]), + "Listen", listen.split(" ")) + self.save_notes += ("Added Listen %s directive to " + "%s\n") % (listen, + self.parser.loc["listen"]) def _has_port_already(self, listens, port): """Helper method for prepare_server_https to find out if user From 0bfdea86d634109d63b12ac4ad593c76c04f1d80 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 20 Jun 2016 14:32:21 -0700 Subject: [PATCH 352/396] Bump cryptography version --- letsencrypt-auto-source/letsencrypt-auto | 45 ++++++++++--------- .../pieces/letsencrypt-auto-requirements.txt | 45 ++++++++++--------- 2 files changed, 46 insertions(+), 44 deletions(-) diff --git a/letsencrypt-auto-source/letsencrypt-auto b/letsencrypt-auto-source/letsencrypt-auto index 80b81c898..fa3996e9e 100755 --- a/letsencrypt-auto-source/letsencrypt-auto +++ b/letsencrypt-auto-source/letsencrypt-auto @@ -598,28 +598,29 @@ ConfigArgParse==0.10.0 \ --hash=sha256:3b50a83dd58149dfcee98cb6565265d10b53e9c0a2bca7eeef7fb5f5524890a7 configobj==5.0.6 \ --hash=sha256:a2f5650770e1c87fb335af19a9b7eb73fc05ccf22144eb68db7d00cd2bcb0902 -cryptography==1.2.3 \ - --hash=sha256:031938f73a5c5eb3e809e18ff7caeb6865351871417be6050cb8c86a9a202b9a \ - --hash=sha256:a179a38d50f8d68b491d7a313db78f8cabe290842cecddddc7b34d408e59db0a \ - --hash=sha256:906c88b2aadcf99cfabb24098263d1bf65ab0c8688acde10dae1f09d865920f1 \ - --hash=sha256:6e706c5c6088770b1d1b634e959e21963e315b0255f5f4777125ad3d54082977 \ - --hash=sha256:f5ebf8e31c48f8707921dca0e994de77813a9c9b9bf03c119c5ddf97bdcffe73 \ - --hash=sha256:c7b89e42288cc7fbee3812e99ef5c744f22452e11d6822f6807afc6d6b3be83e \ - --hash=sha256:8408d29865947109d8b68f1837a7cde1aa4dc86e0f79ca3ba58c0c44e443d6a5 \ - --hash=sha256:c7e76cf3c3d925dd31fa238cfb806cffba718c0f08707d77a538768477969956 \ - --hash=sha256:7d8de35380f31702758b7753bb5c40723832c73006dedb2f9099bf61a37f7287 \ - --hash=sha256:5edbee71fae5469ee83fe0a37866b9398c8ce3a46325c24fcedfbf097bb48a19 \ - --hash=sha256:594edafe4801c13bdc1cc305e7704a90c19617e95936f6ab457ee4ffe000ba50 \ - --hash=sha256:b7fdb16a0a7f481be42da744bfe1ea2163025de21f90f2c688a316f3c354da9c \ - --hash=sha256:207b8bf0fe0907336df38b733b487521cf9e138189aba9234ad54fe545dd0db8 \ - --hash=sha256:509a2f05386270cf783993c90d49ffefb3dd62aee45bf1ea8ce3d2cde7271c21 \ - --hash=sha256:ac69b65dd1af0179ede40c9f15788c88f73e628ea6c0519de3838e279bb388c6 \ - --hash=sha256:8df6fad6c6ae12fd7004ea29357f0a2b4d3774eaeca7656530d08d2d90cd41aa \ - --hash=sha256:0b8b96dd81cc1533a04f30382c0fe21c1972e189f794d0c4261a18cec08fd9b5 \ - --hash=sha256:cae8fca1883f23c50ea78d89de6fe4fefdb4cea83177760f47177559414ded93 \ - --hash=sha256:1a471ca576a9cdce1b1cd9f3a22b1d09ee44d46862037557de17919c0db44425 \ - --hash=sha256:8ec4e8e3d453b3a1b63b5f57737a434dcf1ee4a2f26f6ff7c5a37c3f679104d2 \ - --hash=sha256:8eb11c77dd8e73f48df6b2f7a7e16173fe0fe8fdfe266232832e88477e08454e +cryptography==1.3.4 \ + --hash=sha256:bede00edd11a2a62c8c98c271cc103fa3a3d72acf64f6e5e4eaf251128897b17 \ + --hash=sha256:53b39e687b744bb548a98f40736cc529d9f60959b4e6cc551322cf9505d35eb3 \ + --hash=sha256:474b73ad1139b4e423e46bbd818efd0d5c0df1c65d9f7c957d64c9215d77afde \ + --hash=sha256:aaddf9592d5b99e32dd518bb4a25b147c124f9d6b4ad64b94f01b15d1666b8c8 \ + --hash=sha256:6dcad2f407db8c3cd6ecd78361439c449a4f94786b46c54507e7e68f51e1709d \ + --hash=sha256:475c153fc622e656f1f10a9c9941d0ac7ab18df7c38d35d563a437c1c0e34f24 \ + --hash=sha256:86dd61df581cba04e89e45081efbc531faff1c9d99c77b1ce97f87216c356353 \ + --hash=sha256:75cc697e4ef5fdd0102ca749114c6370dbd11db0c9132a18834858c2566247e3 \ + --hash=sha256:ea03ad5b9df6d79fc9fc1ab23729e01e1c920d2974c5e3c634ccf45a5c378452 \ + --hash=sha256:c8872b8fe4f3416d6338ab99612f49ab314f7856cb43bffab2a32d28a6267be8 \ + --hash=sha256:468fc6e16eaec6ceaa6bc341273e6e9912d01b42b740f8cf896ace7fcd6a321d \ + --hash=sha256:d6fea3c6502735011c5d61a62aef1c1d770fc6a2def45d9e6c0d94c9651e3317 \ + --hash=sha256:3cf95f179f4bead3d5649b91860ef4cf60ad4244209190fc405908272576d961 \ + --hash=sha256:141f77e60a5b9158309b2b60288c7f81d37faa15c22a69b94c190ceefaaa6236 \ + --hash=sha256:87b7a1fe703c6424451f3372d1879dae91c7fe5e13375441a72833db76fee30e \ + --hash=sha256:f5ee3cb0cf1a6550bf483ccffa6608db267a377b45f7e3a8201a86d1d8feb19f \ + --hash=sha256:4e097286651ea318300af3251375d48b71b8228481c56cd617ddd4459a1ff261 \ + --hash=sha256:1e3d3ae3f22f22d50d340f47f25227511326f3f1396c6d2446a5b45b516c4313 \ + --hash=sha256:6a057941cb64d79834ea3cf99093fcc4787c2a5d44f686c4f297361ddc419bcd \ + --hash=sha256:68b3d5390b92559ddd3353c73ab2dfcff758f9c4ec4f5d5226ccede0e5d779f4 \ + --hash=sha256:545dc003b4b6081f9c3e452da15d819b04b696f49484aff64c0a2aedf766bef8 \ + --hash=sha256:423ff890c01be7c70dbfeaa967eeef5146f1a43a5f810ffdc07b178e48a105a9 enum34==1.1.2 \ --hash=sha256:2475d7fcddf5951e92ff546972758802de5260bf409319a9f1934e6bbc8b1dc7 \ --hash=sha256:35907defb0f992b75ab7788f65fedc1cf20ffa22688e0e6f6f12afc06b3ea501 diff --git a/letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt b/letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt index 1291b2c96..2485491da 100644 --- a/letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt +++ b/letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt @@ -32,28 +32,29 @@ ConfigArgParse==0.10.0 \ --hash=sha256:3b50a83dd58149dfcee98cb6565265d10b53e9c0a2bca7eeef7fb5f5524890a7 configobj==5.0.6 \ --hash=sha256:a2f5650770e1c87fb335af19a9b7eb73fc05ccf22144eb68db7d00cd2bcb0902 -cryptography==1.2.3 \ - --hash=sha256:031938f73a5c5eb3e809e18ff7caeb6865351871417be6050cb8c86a9a202b9a \ - --hash=sha256:a179a38d50f8d68b491d7a313db78f8cabe290842cecddddc7b34d408e59db0a \ - --hash=sha256:906c88b2aadcf99cfabb24098263d1bf65ab0c8688acde10dae1f09d865920f1 \ - --hash=sha256:6e706c5c6088770b1d1b634e959e21963e315b0255f5f4777125ad3d54082977 \ - --hash=sha256:f5ebf8e31c48f8707921dca0e994de77813a9c9b9bf03c119c5ddf97bdcffe73 \ - --hash=sha256:c7b89e42288cc7fbee3812e99ef5c744f22452e11d6822f6807afc6d6b3be83e \ - --hash=sha256:8408d29865947109d8b68f1837a7cde1aa4dc86e0f79ca3ba58c0c44e443d6a5 \ - --hash=sha256:c7e76cf3c3d925dd31fa238cfb806cffba718c0f08707d77a538768477969956 \ - --hash=sha256:7d8de35380f31702758b7753bb5c40723832c73006dedb2f9099bf61a37f7287 \ - --hash=sha256:5edbee71fae5469ee83fe0a37866b9398c8ce3a46325c24fcedfbf097bb48a19 \ - --hash=sha256:594edafe4801c13bdc1cc305e7704a90c19617e95936f6ab457ee4ffe000ba50 \ - --hash=sha256:b7fdb16a0a7f481be42da744bfe1ea2163025de21f90f2c688a316f3c354da9c \ - --hash=sha256:207b8bf0fe0907336df38b733b487521cf9e138189aba9234ad54fe545dd0db8 \ - --hash=sha256:509a2f05386270cf783993c90d49ffefb3dd62aee45bf1ea8ce3d2cde7271c21 \ - --hash=sha256:ac69b65dd1af0179ede40c9f15788c88f73e628ea6c0519de3838e279bb388c6 \ - --hash=sha256:8df6fad6c6ae12fd7004ea29357f0a2b4d3774eaeca7656530d08d2d90cd41aa \ - --hash=sha256:0b8b96dd81cc1533a04f30382c0fe21c1972e189f794d0c4261a18cec08fd9b5 \ - --hash=sha256:cae8fca1883f23c50ea78d89de6fe4fefdb4cea83177760f47177559414ded93 \ - --hash=sha256:1a471ca576a9cdce1b1cd9f3a22b1d09ee44d46862037557de17919c0db44425 \ - --hash=sha256:8ec4e8e3d453b3a1b63b5f57737a434dcf1ee4a2f26f6ff7c5a37c3f679104d2 \ - --hash=sha256:8eb11c77dd8e73f48df6b2f7a7e16173fe0fe8fdfe266232832e88477e08454e +cryptography==1.3.4 \ + --hash=sha256:bede00edd11a2a62c8c98c271cc103fa3a3d72acf64f6e5e4eaf251128897b17 \ + --hash=sha256:53b39e687b744bb548a98f40736cc529d9f60959b4e6cc551322cf9505d35eb3 \ + --hash=sha256:474b73ad1139b4e423e46bbd818efd0d5c0df1c65d9f7c957d64c9215d77afde \ + --hash=sha256:aaddf9592d5b99e32dd518bb4a25b147c124f9d6b4ad64b94f01b15d1666b8c8 \ + --hash=sha256:6dcad2f407db8c3cd6ecd78361439c449a4f94786b46c54507e7e68f51e1709d \ + --hash=sha256:475c153fc622e656f1f10a9c9941d0ac7ab18df7c38d35d563a437c1c0e34f24 \ + --hash=sha256:86dd61df581cba04e89e45081efbc531faff1c9d99c77b1ce97f87216c356353 \ + --hash=sha256:75cc697e4ef5fdd0102ca749114c6370dbd11db0c9132a18834858c2566247e3 \ + --hash=sha256:ea03ad5b9df6d79fc9fc1ab23729e01e1c920d2974c5e3c634ccf45a5c378452 \ + --hash=sha256:c8872b8fe4f3416d6338ab99612f49ab314f7856cb43bffab2a32d28a6267be8 \ + --hash=sha256:468fc6e16eaec6ceaa6bc341273e6e9912d01b42b740f8cf896ace7fcd6a321d \ + --hash=sha256:d6fea3c6502735011c5d61a62aef1c1d770fc6a2def45d9e6c0d94c9651e3317 \ + --hash=sha256:3cf95f179f4bead3d5649b91860ef4cf60ad4244209190fc405908272576d961 \ + --hash=sha256:141f77e60a5b9158309b2b60288c7f81d37faa15c22a69b94c190ceefaaa6236 \ + --hash=sha256:87b7a1fe703c6424451f3372d1879dae91c7fe5e13375441a72833db76fee30e \ + --hash=sha256:f5ee3cb0cf1a6550bf483ccffa6608db267a377b45f7e3a8201a86d1d8feb19f \ + --hash=sha256:4e097286651ea318300af3251375d48b71b8228481c56cd617ddd4459a1ff261 \ + --hash=sha256:1e3d3ae3f22f22d50d340f47f25227511326f3f1396c6d2446a5b45b516c4313 \ + --hash=sha256:6a057941cb64d79834ea3cf99093fcc4787c2a5d44f686c4f297361ddc419bcd \ + --hash=sha256:68b3d5390b92559ddd3353c73ab2dfcff758f9c4ec4f5d5226ccede0e5d779f4 \ + --hash=sha256:545dc003b4b6081f9c3e452da15d819b04b696f49484aff64c0a2aedf766bef8 \ + --hash=sha256:423ff890c01be7c70dbfeaa967eeef5146f1a43a5f810ffdc07b178e48a105a9 enum34==1.1.2 \ --hash=sha256:2475d7fcddf5951e92ff546972758802de5260bf409319a9f1934e6bbc8b1dc7 \ --hash=sha256:35907defb0f992b75ab7788f65fedc1cf20ffa22688e0e6f6f12afc06b3ea501 From 8b3528969d4c0939e6f5cd4b4f77d873b2d50283 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 20 Jun 2016 14:33:00 -0700 Subject: [PATCH 353/396] Bump pyopenssl version --- letsencrypt-auto-source/letsencrypt-auto | 6 +++--- .../pieces/letsencrypt-auto-requirements.txt | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/letsencrypt-auto-source/letsencrypt-auto b/letsencrypt-auto-source/letsencrypt-auto index fa3996e9e..a25b3a70e 100755 --- a/letsencrypt-auto-source/letsencrypt-auto +++ b/letsencrypt-auto-source/letsencrypt-auto @@ -680,9 +680,9 @@ pyasn1==0.1.9 \ --hash=sha256:5191ff6b9126d2c039dd87f8ff025bed274baf07fa78afa46f556b1ad7265d6e \ --hash=sha256:8323e03637b2d072cc7041300bac6ec448c3c28950ab40376036788e9a1af629 \ --hash=sha256:853cacd96d1f701ddd67aa03ecc05f51890135b7262e922710112f12a2ed2a7f -pyOpenSSL==0.15.1 \ - --hash=sha256:88e45e6bb25dfed272a1ef2e728461d44b634c2cd689e989b6e56a349c5a3ae5 \ - --hash=sha256:f0a26070d6db0881de8bcc7846934b7c3c930d8f9c79d45883ee48984bc0d672 +pyopenssl==16.0.0 \ + --hash=sha256:5add70cf00273bf957ca31fdb0df9b0ae4639e081897d5f86a0ae1f104901230 \ + --hash=sha256:363d10ee43d062285facf4e465f4f5163f9f702f9134f0a5896f134cbb92d17d pyRFC3339==1.0 \ --hash=sha256:eea31835c56e2096af4363a5745a784878a61d043e247d3a6d6a0a32a9741f56 \ --hash=sha256:8dfbc6c458b8daba1c0f3620a8c78008b323a268b27b7359e92a4ae41325f535 diff --git a/letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt b/letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt index 2485491da..0c642a33e 100644 --- a/letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt +++ b/letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt @@ -114,9 +114,9 @@ pyasn1==0.1.9 \ --hash=sha256:5191ff6b9126d2c039dd87f8ff025bed274baf07fa78afa46f556b1ad7265d6e \ --hash=sha256:8323e03637b2d072cc7041300bac6ec448c3c28950ab40376036788e9a1af629 \ --hash=sha256:853cacd96d1f701ddd67aa03ecc05f51890135b7262e922710112f12a2ed2a7f -pyOpenSSL==0.15.1 \ - --hash=sha256:88e45e6bb25dfed272a1ef2e728461d44b634c2cd689e989b6e56a349c5a3ae5 \ - --hash=sha256:f0a26070d6db0881de8bcc7846934b7c3c930d8f9c79d45883ee48984bc0d672 +pyopenssl==16.0.0 \ + --hash=sha256:5add70cf00273bf957ca31fdb0df9b0ae4639e081897d5f86a0ae1f104901230 \ + --hash=sha256:363d10ee43d062285facf4e465f4f5163f9f702f9134f0a5896f134cbb92d17d pyRFC3339==1.0 \ --hash=sha256:eea31835c56e2096af4363a5745a784878a61d043e247d3a6d6a0a32a9741f56 \ --hash=sha256:8dfbc6c458b8daba1c0f3620a8c78008b323a268b27b7359e92a4ae41325f535 From ef31a837f7242691e8c6ae83f6a92a3cf6dc5792 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Mon, 20 Jun 2016 15:56:19 -0700 Subject: [PATCH 354/396] Lint --- certbot-nginx/certbot_nginx/nginxparser.py | 8 ++++---- certbot-nginx/certbot_nginx/tests/util.py | 1 - 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/certbot-nginx/certbot_nginx/nginxparser.py b/certbot-nginx/certbot_nginx/nginxparser.py index d0f53e1a8..f74f3066e 100644 --- a/certbot-nginx/certbot_nginx/nginxparser.py +++ b/certbot-nginx/certbot_nginx/nginxparser.py @@ -198,7 +198,7 @@ class UnspacedList(list): if not isinstance(inbound, list): # str or None return (inbound, inbound) else: - if not hasattr(x, "spaced"): + if not hasattr(inbound, "spaced"): inbound = UnspacedList(inbound) return (inbound, inbound.spaced) @@ -224,9 +224,9 @@ class UnspacedList(list): return l def __setitem__(self, i, value): - item, spaced_item = self._coerce(x) - self.spaced.__setitem__(i + self._spaces_before(i), value) - list.__setitem__(self, i, value) + item, spaced_item = self._coerce(value) + self.spaced.__setitem__(i + self._spaces_before(i), spaced_item) + list.__setitem__(self, i, item) def __delitem__(self, i): self.spaced.__delitem__(i + self._spaces_before(i)) diff --git a/certbot-nginx/certbot_nginx/tests/util.py b/certbot-nginx/certbot_nginx/tests/util.py index 4ea4a03a7..866e5a9c7 100644 --- a/certbot-nginx/certbot_nginx/tests/util.py +++ b/certbot-nginx/certbot_nginx/tests/util.py @@ -17,7 +17,6 @@ from certbot.plugins import common from certbot_nginx import constants from certbot_nginx import configurator -from certbot_nginx import nginxparser class NginxTest(unittest.TestCase): # pylint: disable=too-few-public-methods From e2fd1369f33dc8e8adbbf40562f5e90e3811bcb2 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Mon, 20 Jun 2016 16:08:40 -0700 Subject: [PATCH 355/396] Update test for fancier semantics --- certbot-nginx/certbot_nginx/tests/nginxparser_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certbot-nginx/certbot_nginx/tests/nginxparser_test.py b/certbot-nginx/certbot_nginx/tests/nginxparser_test.py index 2e8011443..1b1073de0 100644 --- a/certbot-nginx/certbot_nginx/tests/nginxparser_test.py +++ b/certbot-nginx/certbot_nginx/tests/nginxparser_test.py @@ -207,7 +207,7 @@ class TestUnspacedList(unittest.TestCase): self.assertEqual(ul3.spaced, self.a + self.b) self.assertEqual(self.ul.spaced, self.a) ul3 = self.ul + self.l2 - self.assertEqual(ul3, ["things", "quirk", "y", " "]) + self.assertEqual(ul3, ["things", "quirk", "y"]) self.assertEqual(ul3.spaced, self.a + self.b) def test_extend(self): From 02844904f0bd4a11f2d5f24e69033bbb580b0a9f Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Mon, 20 Jun 2016 16:11:32 -0700 Subject: [PATCH 356/396] Improve coverage --- certbot-nginx/certbot_nginx/tests/nginxparser_test.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/certbot-nginx/certbot_nginx/tests/nginxparser_test.py b/certbot-nginx/certbot_nginx/tests/nginxparser_test.py index 1b1073de0..4c239ac10 100644 --- a/certbot-nginx/certbot_nginx/tests/nginxparser_test.py +++ b/certbot-nginx/certbot_nginx/tests/nginxparser_test.py @@ -115,13 +115,7 @@ class TestRawNginxParser(unittest.TestCase): def test_dump_as_file(self): with open(util.get_data_filename('nginx.conf')) as handle: - try: - parsed = load(handle) - except: - handle.seek(0) - print "Failed on", handle.read() - raise - #parsed = util.filter_comments(parsed) + parsed = load(handle) parsed[-1][-1].append(UnspacedList([['server'], [['listen', ' ', '443 ssl'], ['server_name', ' ', 'localhost'], From 14ea8d8cdf8a63870b741886af3da21b946d9775 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Mon, 20 Jun 2016 17:45:02 -0700 Subject: [PATCH 357/396] Fix server_name spaciness --- certbot-nginx/certbot_nginx/configurator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certbot-nginx/certbot_nginx/configurator.py b/certbot-nginx/certbot_nginx/configurator.py index 4f46b6a66..dd9a44aad 100644 --- a/certbot-nginx/certbot_nginx/configurator.py +++ b/certbot-nginx/certbot_nginx/configurator.py @@ -225,7 +225,7 @@ class NginxConfigurator(common.Plugin): if not matches: # No matches. Create a new vhost with this name in nginx.conf. filep = self.parser.loc["root"] - new_block = [['server'], [['server_name', target_name]]] + new_block = [['server'], [['\n', 'server_name', ' ', target_name]]] self.parser.add_http_directives(filep, new_block) vhost = obj.VirtualHost(filep, set([]), False, True, set([target_name]), list(new_block[1])) From 5960376d36f4ccec38814c87bf972c01a23f10b0 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Mon, 20 Jun 2016 18:17:47 -0700 Subject: [PATCH 358/396] Cleanups --- certbot-nginx/certbot_nginx/nginxparser.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/certbot-nginx/certbot_nginx/nginxparser.py b/certbot-nginx/certbot_nginx/nginxparser.py index f74f3066e..5c2bec577 100644 --- a/certbot-nginx/certbot_nginx/nginxparser.py +++ b/certbot-nginx/certbot_nginx/nginxparser.py @@ -62,9 +62,8 @@ class RawNginxParser(object): class RawNginxDumper(object): # pylint: disable=too-few-public-methods """A class that dumps nginx configuration from the provided tree.""" - def __init__(self, blocks, indentation=0): + def __init__(self, blocks): self.blocks = blocks - self.indentation = indentation def __iter__(self, blocks=None): """Iterates the dumped nginx content.""" @@ -100,10 +99,6 @@ class RawNginxDumper(object): if values and spacey(values): gap = values values = b.pop(0) - #if values is None: - # yield indentation + key + gap + ';' - #else: - # yield indentation + key + gap + values + ';' yield indentation + key + gap + values + ';' def __str__(self): From 56488b189901c2d9f760e241586cab7517f75d07 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Mon, 20 Jun 2016 18:18:25 -0700 Subject: [PATCH 359/396] Explain the most likely cause of a missing replay nonce error --- acme/acme/errors.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/acme/acme/errors.py b/acme/acme/errors.py index 77d47c522..ca2ab1874 100644 --- a/acme/acme/errors.py +++ b/acme/acme/errors.py @@ -49,7 +49,7 @@ class MissingNonce(NonceError): def __str__(self): return ('Server {0} response did not include a replay ' - 'nonce, headers: {1}'.format( + 'nonce, headers: {1}\n(This may be a service outage)'.format( self.response.request.method, self.response.headers)) From 884b21ffbe3b577eb82f2da58793b31e7d5dd6b8 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Tue, 21 Jun 2016 15:11:32 -0700 Subject: [PATCH 360/396] fix docstring typo --- certbot-nginx/certbot_nginx/parser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certbot-nginx/certbot_nginx/parser.py b/certbot-nginx/certbot_nginx/parser.py index 31595d56d..3c3942d22 100644 --- a/certbot-nginx/certbot_nginx/parser.py +++ b/certbot-nginx/certbot_nginx/parser.py @@ -18,7 +18,7 @@ logger = logging.getLogger(__name__) class NginxParser(object): """Class handles the fine details of parsing the Nginx Configuration. - :ivar str root: Normalized abosulte path to the server root + :ivar str root: Normalized absolute path to the server root directory. Without trailing slash. :ivar dict parsed: Mapping of file paths to parsed trees From 098d23ac984d9c242dcaf3436ac1d13dac579769 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Tue, 21 Jun 2016 15:33:57 -0700 Subject: [PATCH 361/396] Comment a couple of things --- certbot-nginx/certbot_nginx/parser.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/certbot-nginx/certbot_nginx/parser.py b/certbot-nginx/certbot_nginx/parser.py index 3c3942d22..a0c29fc59 100644 --- a/certbot-nginx/certbot_nginx/parser.py +++ b/certbot-nginx/certbot_nginx/parser.py @@ -155,7 +155,9 @@ class NginxParser(object): :rtype: list """ - files = glob.glob(filepath) + files = glob.glob(filepath) # nginx on unix calls glob(3) for this + # XXX Windows nginx uses FindFirstFile, and + # should have a narrower call here trees = [] for item in files: if item in self.parsed and not override: @@ -210,6 +212,8 @@ class NginxParser(object): empty, this overrides the existing conf files. """ + # XXX probably this should be modified to perform atomic write + # operations and/or to defer control-c until completed for filename in self.parsed: tree = self.parsed[filename] if ext: From 7ea0ce5a1774bf3fadf1e5e2313bd3190a25a80f Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Tue, 21 Jun 2016 15:34:26 -0700 Subject: [PATCH 362/396] Remove warning about nginx options file - This would be a security issue if we ran as setuid root at the request of non-privileged users, but we don't --- certbot-nginx/certbot_nginx/configurator.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/certbot-nginx/certbot_nginx/configurator.py b/certbot-nginx/certbot_nginx/configurator.py index 30928e56c..26640a527 100644 --- a/certbot-nginx/certbot_nginx/configurator.py +++ b/certbot-nginx/certbot_nginx/configurator.py @@ -689,11 +689,6 @@ def nginx_restart(nginx_ctl, nginx_conf="/etc/nginx.conf"): def temp_install(options_ssl): """Temporary install for convenience.""" - # WARNING: THIS IS A POTENTIAL SECURITY VULNERABILITY - # THIS SHOULD BE HANDLED BY THE PACKAGE MANAGER - # AND TAKEN OUT BEFORE RELEASE, INSTEAD - # SHOWING A NICE ERROR MESSAGE ABOUT THE PROBLEM. - # Check to make sure options-ssl.conf is installed if not os.path.isfile(options_ssl): shutil.copyfile(constants.MOD_SSL_CONF_SRC, options_ssl) From 1caf3e993517554d2cde2fdc4ab071bccf923c4b Mon Sep 17 00:00:00 2001 From: Dominic Cleal Date: Wed, 22 Jun 2016 09:29:37 +0100 Subject: [PATCH 363/396] Merge Augeas fix for comment line continuations From https://github.com/hercules-team/augeas/commit/64189250e2c49442a0875fa46dfb1d76a4371bde Fixes #2050 --- .../certbot_apache/augeas_lens/httpd.aug | 5 +- .../passing/comment-continuations-2050.conf | 428 ++++++++++++++++++ 2 files changed, 432 insertions(+), 1 deletion(-) create mode 100644 certbot-apache/certbot_apache/tests/apache-conf-files/passing/comment-continuations-2050.conf diff --git a/certbot-apache/certbot_apache/augeas_lens/httpd.aug b/certbot-apache/certbot_apache/augeas_lens/httpd.aug index 7a5129b56..2729f4b60 100644 --- a/certbot-apache/certbot_apache/augeas_lens/httpd.aug +++ b/certbot-apache/certbot_apache/augeas_lens/httpd.aug @@ -52,11 +52,14 @@ let sep_eq = del /[ \t]*=[ \t]*/ "=" let nmtoken = /[a-zA-Z:_][a-zA-Z0-9:_.-]*/ let word = /[a-z][a-z0-9._-]*/i -let comment = Util.comment let eol = Util.doseol let empty = Util.empty_dos let indent = Util.indent +let comment_val_re = /([^ \t\r\n](.|\\\\\r?\n)*[^ \\\t\r\n]|[^ \t\r\n])/ +let comment = [ label "#comment" . del /[ \t]*#[ \t]*/ "# " + . store comment_val_re . eol ] + (* borrowed from shellvars.aug *) let char_arg_dir = /([^\\ '"{\t\r\n]|[^ '"{\t\r\n]+[^\\ \t\r\n])|\\\\"|\\\\'|\\\\ / let char_arg_sec = /([^\\ '"\t\r\n>]|[^ '"\t\r\n>]+[^\\ \t\r\n>])|\\\\"|\\\\'|\\\\ / diff --git a/certbot-apache/certbot_apache/tests/apache-conf-files/passing/comment-continuations-2050.conf b/certbot-apache/certbot_apache/tests/apache-conf-files/passing/comment-continuations-2050.conf new file mode 100644 index 000000000..48b344d8a --- /dev/null +++ b/certbot-apache/certbot_apache/tests/apache-conf-files/passing/comment-continuations-2050.conf @@ -0,0 +1,428 @@ +# --------------------------------------------------------------- +# Core ModSecurity Rule Set ver.2.2.6 +# Copyright (C) 2006-2012 Trustwave All rights reserved. +# +# The OWASP ModSecurity Core Rule Set is distributed under +# Apache Software License (ASL) version 2 +# Please see the enclosed LICENCE file for full details. +# --------------------------------------------------------------- + + +# +# -- [[ Recommended Base Configuration ]] ------------------------------------------------- +# +# The configuration directives/settings in this file are used to control +# the OWASP ModSecurity CRS. These settings do **NOT** configure the main +# ModSecurity settings such as: +# +# - SecRuleEngine +# - SecRequestBodyAccess +# - SecAuditEngine +# - SecDebugLog +# +# You should use the modsecurity.conf-recommended file that comes with the +# ModSecurity source code archive. +# +# Ref: http://mod-security.svn.sourceforge.net/viewvc/mod-security/m2/trunk/modsecurity.conf-recommended +# + + +# +# -- [[ Rule Version ]] ------------------------------------------------------------------- +# +# Rule version data is added to the "Producer" line of Section H of the Audit log: +# +# - Producer: ModSecurity for Apache/2.7.0-rc1 (http://www.modsecurity.org/); OWASP_CRS/2.2.4. +# +# Ref: https://sourceforge.net/apps/mediawiki/mod-security/index.php?title=Reference_Manual#SecComponentSignature +# +#SecComponentSignature "OWASP_CRS/2.2.6" + + +# +# -- [[ Modes of Operation: Self-Contained vs. Collaborative Detection ]] ----------------- +# +# Each detection rule uses the "block" action which will inherit the SecDefaultAction +# specified below. Your settings here will determine which mode of operation you use. +# +# -- [[ Self-Contained Mode ]] -- +# Rules inherit the "deny" disruptive action. The first rule that matches will block. +# +# -- [[ Collaborative Detection Mode ]] -- +# This is a "delayed blocking" mode of operation where each matching rule will inherit +# the "pass" action and will only contribute to anomaly scores. Transactional blocking +# can be applied +# +# -- [[ Alert Logging Control ]] -- +# You have three options - +# +# - To log to both the Apache error_log and ModSecurity audit_log file use: "log" +# - To log *only* to the ModSecurity audit_log file use: "nolog,auditlog" +# - To log *only* to the Apache error_log file use: "log,noauditlog" +# +# Ref: http://blog.spiderlabs.com/2010/11/advanced-topic-of-the-week-traditional-vs-anomaly-scoring-detection-modes.html +# Ref: https://sourceforge.net/apps/mediawiki/mod-security/index.php?title=Reference_Manual#SecDefaultAction +# +#SecDefaultAction "phase:1,deny,log" + + +# +# -- [[ Collaborative Detection Severity Levels ]] ---------------------------------------- +# +# These are the default scoring points for each severity level. You may +# adjust these to you liking. These settings will be used in macro expansion +# in the rules to increment the anomaly scores when rules match. +# +# These are the default Severity ratings (with anomaly scores) of the individual rules - +# +# - 2: Critical - Anomaly Score of 5. +# Is the highest severity level possible without correlation. It is +# normally generated by the web attack rules (40 level files). +# - 3: Error - Anomaly Score of 4. +# Is generated mostly from outbound leakage rules (50 level files). +# - 4: Warning - Anomaly Score of 3. +# Is generated by malicious client rules (35 level files). +# - 5: Notice - Anomaly Score of 2. +# Is generated by the Protocol policy and anomaly files. +# +#SecAction \ + "id:'900001', \ + phase:1, \ + t:none, \ + setvar:tx.critical_anomaly_score=5, \ + setvar:tx.error_anomaly_score=4, \ + setvar:tx.warning_anomaly_score=3, \ + setvar:tx.notice_anomaly_score=2, \ + nolog, \ + pass" + + +# +# -- [[ Collaborative Detection Scoring Threshold Levels ]] ------------------------------ +# +# These variables are used in macro expansion in the 49 inbound blocking and 59 +# outbound blocking files. +# +# **MUST HAVE** ModSecurity v2.5.12 or higher to use macro expansion in numeric +# operators. If you have an earlier version, edit the 49/59 files directly to +# set the appropriate anomaly score levels. +# +# You should set the score to the proper threshold you would prefer. If set to "5" +# it will work similarly to previous Mod CRS rules and will create an event in the error_log +# file if there are any rules that match. If you would like to lessen the number of events +# generated in the error_log file, you should increase the anomaly score threshold to +# something like "20". This would only generate an event in the error_log file if +# there are multiple lower severity rule matches or if any 1 higher severity item matches. +# +#SecAction \ + "id:'900002', \ + phase:1, \ + t:none, \ + setvar:tx.inbound_anomaly_score_level=5, \ + nolog, \ + pass" + + +#SecAction \ + "id:'900003', \ + phase:1, \ + t:none, \ + setvar:tx.outbound_anomaly_score_level=4, \ + nolog, \ + pass" + + +# +# -- [[ Collaborative Detection Blocking ]] ----------------------------------------------- +# +# This is a collaborative detection mode where each rule will increment an overall +# anomaly score for the transaction. The scores are then evaluated in the following files: +# +# Inbound anomaly score - checked in the modsecurity_crs_49_inbound_blocking.conf file +# Outbound anomaly score - checked in the modsecurity_crs_59_outbound_blocking.conf file +# +# If you want to use anomaly scoring mode, then uncomment this line. +# +#SecAction \ + "id:'900004', \ + phase:1, \ + t:none, \ + setvar:tx.anomaly_score_blocking=on, \ + nolog, \ + pass" + + +# +# -- [[ GeoIP Database ]] ----------------------------------------------------------------- +# +# There are some rulesets that need to inspect the GEO data of the REMOTE_ADDR data. +# +# You must first download the MaxMind GeoIP Lite City DB - +# +# http://geolite.maxmind.com/download/geoip/database/GeoLiteCity.dat.gz +# +# You then need to define the proper path for the SecGeoLookupDb directive +# +# Ref: http://blog.spiderlabs.com/2010/10/detecting-malice-with-modsecurity-geolocation-data.html +# Ref: http://blog.spiderlabs.com/2010/11/detecting-malice-with-modsecurity-ip-forensics.html +# +#SecGeoLookupDb /opt/modsecurity/lib/GeoLiteCity.dat + +# +# -- [[ Regression Testing Mode ]] -------------------------------------------------------- +# +# If you are going to run the regression testing mode, you should uncomment the +# following rule. It will enable DetectionOnly mode for the SecRuleEngine and +# will enable Response Header tagging so that the client testing script can see +# which rule IDs have matched. +# +# You must specify the your source IP address where you will be running the tests +# from. +# +#SecRule REMOTE_ADDR "@ipMatch 192.168.1.100" \ + "id:'900005', \ + phase:1, \ + t:none, \ + ctl:ruleEngine=DetectionOnly, \ + setvar:tx.regression_testing=1, \ + nolog, \ + pass" + + +# +# -- [[ HTTP Policy Settings ]] ---------------------------------------------------------- +# +# Set the following policy settings here and they will be propagated to the 23 rules +# file (modsecurity_common_23_request_limits.conf) by using macro expansion. +# If you run into false positives, you can adjust the settings here. +# +# Only the max number of args is uncommented by default as there are a high rate +# of false positives. Uncomment the items you wish to set. +# +# +# -- Maximum number of arguments in request limited +#SecAction \ + "id:'900006', \ + phase:1, \ + t:none, \ + setvar:tx.max_num_args=255, \ + nolog, \ + pass" + +# +# -- Limit argument name length +#SecAction \ + "id:'900007', \ + phase:1, \ + t:none, \ + setvar:tx.arg_name_length=100, \ + nolog, \ + pass" + +# +# -- Limit value name length +#SecAction \ + "id:'900008', \ + phase:1, \ + t:none, \ + setvar:tx.arg_length=400, \ + nolog, \ + pass" + +# +# -- Limit arguments total length +#SecAction \ + "id:'900009', \ + phase:1, \ + t:none, \ + setvar:tx.total_arg_length=64000, \ + nolog, \ + pass" + +# +# -- Individual file size is limited +#SecAction \ + "id:'900010', \ + phase:1, \ + t:none, \ + setvar:tx.max_file_size=1048576, \ + nolog, \ + pass" + +# +# -- Combined file size is limited +#SecAction \ + "id:'900011', \ + phase:1, \ + t:none, \ + setvar:tx.combined_file_sizes=1048576, \ + nolog, \ + pass" + + +# +# Set the following policy settings here and they will be propagated to the 30 rules +# file (modsecurity_crs_30_http_policy.conf) by using macro expansion. +# If you run into false positves, you can adjust the settings here. +# +#SecAction \ + "id:'900012', \ + phase:1, \ + t:none, \ + setvar:'tx.allowed_methods=GET HEAD POST OPTIONS', \ + setvar:'tx.allowed_request_content_type=application/x-www-form-urlencoded|multipart/form-data|text/xml|application/xml|application/x-amf', \ + setvar:'tx.allowed_http_versions=HTTP/0.9 HTTP/1.0 HTTP/1.1', \ + setvar:'tx.restricted_extensions=.asa/ .asax/ .ascx/ .axd/ .backup/ .bak/ .bat/ .cdx/ .cer/ .cfg/ .cmd/ .com/ .config/ .conf/ .cs/ .csproj/ .csr/ .dat/ .db/ .dbf/ .dll/ .dos/ .htr/ .htw/ .ida/ .idc/ .idq/ .inc/ .ini/ .key/ .licx/ .lnk/ .log/ .mdb/ .old/ .pass/ .pdb/ .pol/ .printer/ .pwd/ .resources/ .resx/ .sql/ .sys/ .vb/ .vbs/ .vbproj/ .vsdisco/ .webinfo/ .xsd/ .xsx/', \ + setvar:'tx.restricted_headers=/Proxy-Connection/ /Lock-Token/ /Content-Range/ /Translate/ /via/ /if/', \ + nolog, \ + pass" + + +# +# -- [[ Content Security Policy (CSP) Settings ]] ----------------------------------------- +# +# The purpose of these settings is to send CSP response headers to +# Mozilla FireFox users so that you can enforce how dynamic content +# is used. CSP usage helps to prevent XSS attacks against your users. +# +# Reference Link: +# +# https://developer.mozilla.org/en/Security/CSP +# +# Uncomment this SecAction line if you want use CSP enforcement. +# You need to set the appropriate directives and settings for your site/domain and +# and activate the CSP file in the experimental_rules directory. +# +# Ref: http://blog.spiderlabs.com/2011/04/modsecurity-advanced-topic-of-the-week-integrating-content-security-policy-csp.html +# +#SecAction \ + "id:'900013', \ + phase:1, \ + t:none, \ + setvar:tx.csp_report_only=1, \ + setvar:tx.csp_report_uri=/csp_violation_report, \ + setenv:'csp_policy=allow \'self\'; img-src *.yoursite.com; media-src *.yoursite.com; style-src *.yoursite.com; frame-ancestors *.yoursite.com; script-src *.yoursite.com; report-uri %{tx.csp_report_uri}', \ + nolog, \ + pass" + + +# +# -- [[ Brute Force Protection ]] --------------------------------------------------------- +# +# If you are using the Brute Force Protection rule set, then uncomment the following +# lines and set the following variables: +# - Protected URLs: resources to protect (e.g. login pages) - set to your login page +# - Burst Time Slice Interval: time interval window to monitor for bursts +# - Request Threshold: request # threshold to trigger a burst +# - Block Period: temporary block timeout +# +#SecAction \ + "id:'900014', \ + phase:1, \ + t:none, \ + setvar:'tx.brute_force_protected_urls=/login.jsp /partner_login.php', \ + setvar:'tx.brute_force_burst_time_slice=60', \ + setvar:'tx.brute_force_counter_threshold=10', \ + setvar:'tx.brute_force_block_timeout=300', \ + nolog, \ + pass" + + +# +# -- [[ DoS Protection ]] ---------------------------------------------------------------- +# +# If you are using the DoS Protection rule set, then uncomment the following +# lines and set the following variables: +# - Burst Time Slice Interval: time interval window to monitor for bursts +# - Request Threshold: request # threshold to trigger a burst +# - Block Period: temporary block timeout +# +#SecAction \ + "id:'900015', \ + phase:1, \ + t:none, \ + setvar:'tx.dos_burst_time_slice=60', \ + setvar:'tx.dos_counter_threshold=100', \ + setvar:'tx.dos_block_timeout=600', \ + nolog, \ + pass" + + +# +# -- [[ Check UTF enconding ]] ----------------------------------------------------------- +# +# We only want to apply this check if UTF-8 encoding is actually used by the site, otherwise +# it will result in false positives. +# +# Uncomment this line if your site uses UTF8 encoding +#SecAction \ + "id:'900016', \ + phase:1, \ + t:none, \ + setvar:tx.crs_validate_utf8_encoding=1, \ + nolog, \ + pass" + + +# +# -- [[ Enable XML Body Parsing ]] ------------------------------------------------------- +# +# The rules in this file will trigger the XML parser upon an XML request +# +# Initiate XML Processor in case of xml content-type +# +#SecRule REQUEST_HEADERS:Content-Type "text/xml" \ + "id:'900017', \ + phase:1, \ + t:none,t:lowercase, \ + nolog, \ + pass, \ + chain" + #SecRule REQBODY_PROCESSOR "!@streq XML" \ + "ctl:requestBodyProcessor=XML" + + +# +# -- [[ Global and IP Collections ]] ----------------------------------------------------- +# +# Create both Global and IP collections for rules to use +# There are some CRS rules that assume that these two collections +# have already been initiated. +# +#SecRule REQUEST_HEADERS:User-Agent "^(.*)$" \ + "id:'900018', \ + phase:1, \ + t:none,t:sha1,t:hexEncode, \ + setvar:tx.ua_hash=%{matched_var}, \ + nolog, \ + pass" + + +#SecRule REQUEST_HEADERS:x-forwarded-for "^\b(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\b" \ + "id:'900019', \ + phase:1, \ + t:none, \ + capture, \ + setvar:tx.real_ip=%{tx.1}, \ + nolog, \ + pass" + + +#SecRule &TX:REAL_IP "!@eq 0" \ + "id:'900020', \ + phase:1, \ + t:none, \ + initcol:global=global, \ + initcol:ip=%{tx.real_ip}_%{tx.ua_hash}, \ + nolog, \ + pass" + + +#SecRule &TX:REAL_IP "@eq 0" \ + "id:'900021', \ + phase:1, \ + t:none, \ + initcol:global=global, \ + initcol:ip=%{remote_addr}_%{tx.ua_hash}, \ + nolog, \ + pass" From 24cc6b208ae7fb869d3b93ef9d06c68f07a3e848 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Wed, 22 Jun 2016 15:24:33 -0700 Subject: [PATCH 364/396] Avoid newline --- acme/acme/errors.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/acme/acme/errors.py b/acme/acme/errors.py index ca2ab1874..70894a808 100644 --- a/acme/acme/errors.py +++ b/acme/acme/errors.py @@ -49,7 +49,7 @@ class MissingNonce(NonceError): def __str__(self): return ('Server {0} response did not include a replay ' - 'nonce, headers: {1}\n(This may be a service outage)'.format( + 'nonce, headers: {1} (This may be a service outage)'.format( self.response.request.method, self.response.headers)) From 975599e9ca359586990c21a23f999eb93c243d51 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 22 Jun 2016 18:25:09 -0700 Subject: [PATCH 365/396] Remove bad save in nginx perform --- certbot-nginx/certbot_nginx/tests/tls_sni_01_test.py | 2 +- certbot-nginx/certbot_nginx/tls_sni_01.py | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/certbot-nginx/certbot_nginx/tests/tls_sni_01_test.py b/certbot-nginx/certbot_nginx/tests/tls_sni_01_test.py index 3264d6ed3..a92caf788 100644 --- a/certbot-nginx/certbot_nginx/tests/tls_sni_01_test.py +++ b/certbot-nginx/certbot_nginx/tests/tls_sni_01_test.py @@ -80,7 +80,7 @@ class TlsSniPerformTest(util.NginxTest): mock_setup_cert.assert_called_once_with(self.achalls[0]) self.assertEqual([response], responses) - self.assertEqual(mock_save.call_count, 2) + self.assertEqual(mock_save.call_count, 1) # Make sure challenge config is included in main config http = self.sni.configurator.parser.parsed[ diff --git a/certbot-nginx/certbot_nginx/tls_sni_01.py b/certbot-nginx/certbot_nginx/tls_sni_01.py index e4c5d31a6..478810475 100644 --- a/certbot-nginx/certbot_nginx/tls_sni_01.py +++ b/certbot-nginx/certbot_nginx/tls_sni_01.py @@ -46,8 +46,6 @@ class NginxTlsSni01(common.TLSSNI01): if not self.achalls: return [] - self.configurator.save() - addresses = [] default_addr = "{0} default_server ssl".format( self.configurator.config.tls_sni_01_port) From 230b09bb15f5ba264b894b55634140e3c7002f4f Mon Sep 17 00:00:00 2001 From: Jacob Sachs Date: Thu, 23 Jun 2016 08:23:27 -0400 Subject: [PATCH 366/396] add logging for keeping existing cert --- certbot/main.py | 1 + 1 file changed, 1 insertion(+) diff --git a/certbot/main.py b/certbot/main.py index 7d6830b18..90fa62fad 100644 --- a/certbot/main.py +++ b/certbot/main.py @@ -83,6 +83,7 @@ def _auth_from_domains(le_client, config, domains, lineage=None): if action == "reinstall": # The lineage already exists; allow the caller to try installing # it without getting a new certificate at all. + logger.info("Keeping the existing certificate") return lineage, "reinstall" hooks.pre_hook(config) From 4ef7131a4b6868aa32370a27dd8f722a5ae2e429 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Thu, 23 Jun 2016 11:05:49 -0700 Subject: [PATCH 367/396] Name all of nginxparser's magic regexps --- certbot-nginx/certbot_nginx/nginxparser.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/certbot-nginx/certbot_nginx/nginxparser.py b/certbot-nginx/certbot_nginx/nginxparser.py index 5c2bec577..ab57089b9 100644 --- a/certbot-nginx/certbot_nginx/nginxparser.py +++ b/certbot-nginx/certbot_nginx/nginxparser.py @@ -4,7 +4,7 @@ import logging import string from pyparsing import ( - Literal, White, Word, alphanums, CharsNotIn, Forward, Group, + Literal, White, Word, alphanums, CharsNotIn, Combine, Forward, Group, Optional, OneOrMore, Regex, ZeroOrMore) from pyparsing import stringEnd from pyparsing import restOfLine @@ -17,10 +17,13 @@ class RawNginxParser(object): # constants space = Optional(White()) + nonspace = Regex(r"\S+") left_bracket = Literal("{").suppress() right_bracket = space.leaveWhitespace() + Literal("}").suppress() semicolon = Literal(";").suppress() key = Word(alphanums + "_/+-.") + dollar_var = Combine(Literal('$') + nonspace) + condition = Regex(r"\(.+\)") # Matches anything that is not a special character AND any chars in single # or double quotes value = Regex(r"((\".*\")?(\'.*\')?[^\{\};,]?)+") @@ -33,8 +36,8 @@ class RawNginxParser(object): assignment = space + key + Optional(space + value, default=None) + semicolon location_statement = space + Optional(modifier) + Optional(space + location + space) - if_statement = space + Literal("if") + space + Regex(r"\(.+\)") + space - map_statement = space + Literal("map") + space + Regex(r"\S+") + space + Regex(r"\$\S+") + space + if_statement = space + Literal("if") + space + condition + space + map_statement = space + Literal("map") + space + nonspace + space + dollar_var + space block = Forward() block << Group( From db66050a7ae3048f4daf0a06ba6e87231abddcfb Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Thu, 23 Jun 2016 11:39:51 -0700 Subject: [PATCH 368/396] Make atomicity comment more accurate --- certbot-nginx/certbot_nginx/parser.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/certbot-nginx/certbot_nginx/parser.py b/certbot-nginx/certbot_nginx/parser.py index a0c29fc59..f2c10ab70 100644 --- a/certbot-nginx/certbot_nginx/parser.py +++ b/certbot-nginx/certbot_nginx/parser.py @@ -212,8 +212,7 @@ class NginxParser(object): empty, this overrides the existing conf files. """ - # XXX probably this should be modified to perform atomic write - # operations and/or to defer control-c until completed + # Best-effort atomicity is enforced above us by reverter.py for filename in self.parsed: tree = self.parsed[filename] if ext: From a29c6e3102ca1d093b7ffbaed93e88f635907e8d Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Thu, 23 Jun 2016 14:06:32 -0700 Subject: [PATCH 369/396] Try simplifying handling of spacey sublists - this was causing test failures but they may all have been fixed by other changes... --- certbot-nginx/certbot_nginx/nginxparser.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/certbot-nginx/certbot_nginx/nginxparser.py b/certbot-nginx/certbot_nginx/nginxparser.py index ab57089b9..efcdf94ef 100644 --- a/certbot-nginx/certbot_nginx/nginxparser.py +++ b/certbot-nginx/certbot_nginx/nginxparser.py @@ -172,12 +172,7 @@ class UnspacedList(list): for i, entry in reversed(list(enumerate(self))): if isinstance(entry, list): sublist = UnspacedList(entry) - if sublist != [] or sublist.spaced == []: - list.__setitem__(self, i, sublist) - else: - # if a sublist is exclusively spacey entries, it might - # choke the high level parser, so make it disappear - list.__delitem__(self, i) + list.__setitem__(self, i, sublist) self.spaced[i] = sublist.spaced elif spacey(entry): # don't delete comments From 3178a9f0f3365ef4665474a50ccab20b02ec8d91 Mon Sep 17 00:00:00 2001 From: Viktor Szakats Date: Fri, 24 Jun 2016 00:24:53 +0200 Subject: [PATCH 370/396] cli-help: fix spelling --- docs/cli-help.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/cli-help.txt b/docs/cli-help.txt index e5f1fdcb4..749983d0e 100644 --- a/docs/cli-help.txt +++ b/docs/cli-help.txt @@ -147,7 +147,7 @@ security: HTTPS for the newly authenticated vhost. (default: None) --hsts Add the Strict-Transport-Security header to every HTTP - response. Forcing browser to use always use SSL for + response. Forcing browser to always use SSL for the domain. Defends against SSL Stripping. (default: False) --no-hsts Do not automatically add the Strict-Transport-Security From cb441606d52fe3fd67fe8b3f2fd1672cd645ba49 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Thu, 23 Jun 2016 16:14:34 -0700 Subject: [PATCH 371/396] Explictly state assumptions made by certbot --- certbot/interfaces.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/certbot/interfaces.py b/certbot/interfaces.py index 37835462e..a6f0b9735 100644 --- a/certbot/interfaces.py +++ b/certbot/interfaces.py @@ -180,6 +180,9 @@ class IAuthenticator(IPlugin): def cleanup(achalls): """Revert changes and shutdown after challenges complete. + This method should be able to revert all changes made by + perform, even if perform exited abnormally. + :param list achalls: Non-empty (guaranteed) list of :class:`~certbot.achallenges.AnnotatedChallenge` instances, a subset of those previously passed to :func:`perform`. @@ -304,8 +307,11 @@ class IInstaller(IPlugin): Both title and temporary are needed because a save may be intended to be permanent, but the save is not ready to be a full - checkpoint. If an exception is raised, it is assumed a new - checkpoint was not created. + checkpoint. + + It is assumed that at most one checkpoint is finalized by this + method. Additionally, if an exception is raised, it is assumed a + new checkpoint was not finalized. :param str title: The title of the save. If a title is given, the configuration will be saved as a new checkpoint and put in a From 38feb37d3ee0f9b0db689b0db2a2a9df1facd207 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Thu, 23 Jun 2016 17:03:01 -0700 Subject: [PATCH 372/396] Further document reverter --- certbot/reverter.py | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/certbot/reverter.py b/certbot/reverter.py index f8140d60d..c7eab1690 100644 --- a/certbot/reverter.py +++ b/certbot/reverter.py @@ -24,6 +24,39 @@ logger = logging.getLogger(__name__) class Reverter(object): """Reverter Class - save and revert configuration checkpoints. + This class can be used by the plugins, especially installers, to + undo changes made to the user's system. Modifications to files and + commands to do undo actions taken by the plugin should be registered + with this class before the action is taken. + + Once a change has been registered with this class, there are three + states the change can be in. First, the change can be a temporary + change. This should be used for changes that will soon be reverted, + such as config changes for the purpose of solving a challenge. + Changes are added to this state through calls to + :func:`~add_to_temp_checkpoint` and reverted when + :func:`~revert_temporary_config` or :func:`~recovery_routine` is + called. + + The second state a change can be in is in progress. These changes + are not temporary, however, they also have not been finalized in a + checkpoint. A change must become in progress before it can be + finalized. Changes are added to this state through calls to + :func:`~add_to_checkpoint` and reverted when + :func:`~recovery_routine` is called. + + The last state a change can be in is finalized in a checkpoint. A + change is put into this state by first becoming an in progress + change and then calling :func:`~finalize_checkpoint`. Changes + in this state can be reverted through calls to + :func:`~rollback_checkpoints`. + + As a final note, creating new files and registering undo commands + are handled specially and use the methods + :func:`~register_file_creation` and :func:`~register_undo_command` + respectively. Both of these methods can be used to create either + temporary or in progress changes. + .. note:: Consider moving everything over to CSV format. :param config: Configuration. From 7e5e016982a0852f4151c434890102d733eedeba Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Thu, 23 Jun 2016 17:10:03 -0700 Subject: [PATCH 373/396] Document that only save should make checkpoints --- certbot/interfaces.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/certbot/interfaces.py b/certbot/interfaces.py index a6f0b9735..2bc25723c 100644 --- a/certbot/interfaces.py +++ b/certbot/interfaces.py @@ -241,6 +241,11 @@ class IInstaller(IPlugin): Represents any server that an X509 certificate can be placed. + It is assumed that :func:`save` is the only method that finalizes a + checkpoint. This is important to ensure that checkpoints are + restored in a consistent manner if requested by the user or in case + of an error. + """ def get_all_names(): From 7aa6becc5bffa19e85108b0286b08aa29aaf50aa Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Thu, 23 Jun 2016 17:16:58 -0700 Subject: [PATCH 374/396] Suggest reverter for installers --- certbot/interfaces.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/certbot/interfaces.py b/certbot/interfaces.py index 2bc25723c..e4e62e0a2 100644 --- a/certbot/interfaces.py +++ b/certbot/interfaces.py @@ -246,6 +246,9 @@ class IInstaller(IPlugin): restored in a consistent manner if requested by the user or in case of an error. + Using :class:`certbot.reverter.Reverter` to implement checkpoints, + rollback, and recovery can dramatically simplify plugin development. + """ def get_all_names(): From 02c61cffb713ef3609b2dffd8e2465a95cd4fcfe Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Thu, 23 Jun 2016 17:17:17 -0700 Subject: [PATCH 375/396] Consistent capitialization with installer --- certbot/reverter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certbot/reverter.py b/certbot/reverter.py index c7eab1690..6dde05077 100644 --- a/certbot/reverter.py +++ b/certbot/reverter.py @@ -24,7 +24,7 @@ logger = logging.getLogger(__name__) class Reverter(object): """Reverter Class - save and revert configuration checkpoints. - This class can be used by the plugins, especially installers, to + This class can be used by the plugins, especially Installers, to undo changes made to the user's system. Modifications to files and commands to do undo actions taken by the plugin should be registered with this class before the action is taken. From bac988a4042537a2f6429e43b18553b1d0d9f710 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Thu, 23 Jun 2016 17:24:05 -0700 Subject: [PATCH 376/396] Fix typo --- certbot/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certbot/cli.py b/certbot/cli.py index ba1f23708..35b3b74ae 100644 --- a/certbot/cli.py +++ b/certbot/cli.py @@ -773,7 +773,7 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis helpful.add( "security", "--hsts", action="store_true", help="Add the Strict-Transport-Security header to every HTTP response." - " Forcing browser to use always use SSL for the domain." + " Forcing browser to always use SSL for the domain." " Defends against SSL Stripping.", dest="hsts", default=False) helpful.add( "security", "--no-hsts", action="store_false", From 6930523c1469be5b9254405d045e897dfb85ee56 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Thu, 23 Jun 2016 17:59:26 -0700 Subject: [PATCH 377/396] Refactor to simplify indenation handling --- certbot-nginx/certbot_nginx/nginxparser.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/certbot-nginx/certbot_nginx/nginxparser.py b/certbot-nginx/certbot_nginx/nginxparser.py index efcdf94ef..0ec1aeefb 100644 --- a/certbot-nginx/certbot_nginx/nginxparser.py +++ b/certbot-nginx/certbot_nginx/nginxparser.py @@ -41,7 +41,7 @@ class RawNginxParser(object): block = Forward() block << Group( - # XXX could this "key" be Literal("location")? + # key could for instance be "server" or "http" (Group(space + key + location_statement) ^ Group(if_statement) ^ Group(map_statement)).leaveWhitespace() + left_bracket + @@ -78,15 +78,14 @@ class RawNginxDumper(object): b = copy.deepcopy(b0) indentation = "" if spacey(b[0]): - indentation = b.pop(0) + yield b.pop(0) # indentation if not b: - yield indentation continue key = b.pop(0) values = b.pop(0) if isinstance(key, list): - yield indentation + "".join(key) + '{' + yield "".join(key) + '{' for parameter in values: dumped = self.__iter__([parameter]) for line in dumped: @@ -94,7 +93,7 @@ class RawNginxDumper(object): yield '}' else: if isinstance(key, str) and key.strip() == '#': - yield indentation + key + values + yield key + values else: gap = "" # Sometimes the parser has stuck some gap whitespace in here; @@ -102,7 +101,7 @@ class RawNginxDumper(object): if values and spacey(values): gap = values values = b.pop(0) - yield indentation + key + gap + values + ';' + yield key + gap + values + ';' def __str__(self): """Return the parsed block as a string.""" From ad13b525b28542378a8f506732b8ac1146d69b3e Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Fri, 24 Jun 2016 14:35:42 -0700 Subject: [PATCH 378/396] For reference, a buggy attempt to implement slicing but slice assignment seems to mangle the .spaced list in some cases --- certbot-nginx/certbot_nginx/nginxparser.py | 31 +++++++++++++++++----- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/certbot-nginx/certbot_nginx/nginxparser.py b/certbot-nginx/certbot_nginx/nginxparser.py index 0ec1aeefb..3db6ff501 100644 --- a/certbot-nginx/certbot_nginx/nginxparser.py +++ b/certbot-nginx/certbot_nginx/nginxparser.py @@ -76,7 +76,6 @@ class RawNginxDumper(object): yield b0 continue b = copy.deepcopy(b0) - indentation = "" if spacey(b[0]): yield b.pop(0) # indentation if not b: @@ -197,7 +196,7 @@ class UnspacedList(list): def insert(self, i, x): item, spaced_item = self._coerce(x) - self.spaced.insert(i + self._spaces_before(i), spaced_item) + self.spaced.insert(self._spaced_position(i), spaced_item) list.insert(self, i, item) def append(self, x): @@ -215,13 +214,28 @@ class UnspacedList(list): l.extend(other) return l + def __setslice__(self, i, j, newslice): + for idx in reversed(range(self._spaced_position(i), self._spaced_position(j))): + print "delspace", idx + del self.spaced[idx] + for idx in reversed(range(i,j)): + print "del", idx + list.__delitem__(self, idx) + for idx, item in enumerate(newslice): + self.insert(i + idx, item) + def __setitem__(self, i, value): + if isinstance(i, slice): + #raise NotImplementedError("Slice operations on UnspacedLists not yet implemented") + for pos in xrange(i.start or 0, i.stop or len(self), i.step or 1): + self.__setitem__(pos, value) + return item, spaced_item = self._coerce(value) - self.spaced.__setitem__(i + self._spaces_before(i), spaced_item) + self.spaced.__setitem__(self._spaced_position(i), spaced_item) list.__setitem__(self, i, item) def __delitem__(self, i): - self.spaced.__delitem__(i + self._spaces_before(i)) + self.spaced.__delitem__(self._spaced_position(i)) list.__delitem__(self, i) def __deepcopy__(self, unused_memo): @@ -230,14 +244,17 @@ class UnspacedList(list): return l - def _spaces_before(self, idx): - "Count the number of spaces in the spaced list before pos idx in the spaceless one" + def _spaced_position(self, idx): + "Convert from indexes in the unspaced list to positions in the spaced one" + print "lookup", idx spaces = 0 pos = 0 + if idx < 0: + idx = len(self) + idx while idx != -1: if spacey(self.spaced[pos]): spaces += 1 else: idx -= 1 pos += 1 - return spaces + return idx + spaces From e415a4d402d73af1f71171bae844ed435363cacd Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Fri, 24 Jun 2016 14:36:23 -0700 Subject: [PATCH 379/396] For now, instead, consider this NotImplemented --- certbot-nginx/certbot_nginx/nginxparser.py | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/certbot-nginx/certbot_nginx/nginxparser.py b/certbot-nginx/certbot_nginx/nginxparser.py index 3db6ff501..8a423848c 100644 --- a/certbot-nginx/certbot_nginx/nginxparser.py +++ b/certbot-nginx/certbot_nginx/nginxparser.py @@ -215,21 +215,11 @@ class UnspacedList(list): return l def __setslice__(self, i, j, newslice): - for idx in reversed(range(self._spaced_position(i), self._spaced_position(j))): - print "delspace", idx - del self.spaced[idx] - for idx in reversed(range(i,j)): - print "del", idx - list.__delitem__(self, idx) - for idx, item in enumerate(newslice): - self.insert(i + idx, item) + raise NotImplementedError("Slice operations on UnspacedLists not yet implemented") def __setitem__(self, i, value): if isinstance(i, slice): - #raise NotImplementedError("Slice operations on UnspacedLists not yet implemented") - for pos in xrange(i.start or 0, i.stop or len(self), i.step or 1): - self.__setitem__(pos, value) - return + raise NotImplementedError("Slice operations on UnspacedLists not yet implemented") item, spaced_item = self._coerce(value) self.spaced.__setitem__(self._spaced_position(i), spaced_item) list.__setitem__(self, i, item) From 9da533d92e432d6ad365de9dbed7e2810491f577 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Fri, 24 Jun 2016 16:13:12 -0700 Subject: [PATCH 380/396] Be more succinct --- certbot-nginx/certbot_nginx/nginxparser.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/certbot-nginx/certbot_nginx/nginxparser.py b/certbot-nginx/certbot_nginx/nginxparser.py index 8a423848c..f65864df4 100644 --- a/certbot-nginx/certbot_nginx/nginxparser.py +++ b/certbot-nginx/certbot_nginx/nginxparser.py @@ -86,8 +86,7 @@ class RawNginxDumper(object): if isinstance(key, list): yield "".join(key) + '{' for parameter in values: - dumped = self.__iter__([parameter]) - for line in dumped: + for line in self.__iter__([parameter]): # negate "for b0 in blocks" yield line yield '}' else: From 20b92a3c1fba3a922ea3063954b3691d4ed462d7 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Fri, 24 Jun 2016 16:48:22 -0700 Subject: [PATCH 381/396] Add some test coverage --- certbot-nginx/certbot_nginx/tests/configurator_test.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/certbot-nginx/certbot_nginx/tests/configurator_test.py b/certbot-nginx/certbot_nginx/tests/configurator_test.py index a1d0e75cc..7529bc10b 100644 --- a/certbot-nginx/certbot_nginx/tests/configurator_test.py +++ b/certbot-nginx/certbot_nginx/tests/configurator_test.py @@ -265,7 +265,8 @@ class NginxConfiguratorTest(util.NginxTest): @mock.patch("certbot_nginx.configurator.tls_sni_01.NginxTlsSni01.perform") @mock.patch("certbot_nginx.configurator.NginxConfigurator.restart") - def test_perform(self, mock_restart, mock_perform): + @mock.patch("certbot_nginx.configurator.NginxConfigurator.revert_challenge_config") + def test_perform_and_cleanup(self, mock_revert, mock_restart, mock_perform): # Only tests functionality specific to configurator.perform # Note: As more challenges are offered this will have to be expanded achall1 = achallenges.KeyAuthorizationAnnotatedChallenge( @@ -291,7 +292,11 @@ class NginxConfiguratorTest(util.NginxTest): self.assertEqual(mock_perform.call_count, 1) self.assertEqual(responses, expected) - self.assertEqual(mock_restart.call_count, 1) + + self.config.cleanup([achall1, achall2]) + self.assertEqual(0, self.config._chall_out) # pylint: disable=protected-access + self.assertEqual(mock_revert.call_count, 1) + self.assertEqual(mock_restart.call_count, 2) @mock.patch("certbot_nginx.configurator.subprocess.Popen") def test_get_version(self, mock_popen): From 8f0a5fdc66f6b72d210a2edff32c27636bc3ecd7 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Fri, 24 Jun 2016 16:48:30 -0700 Subject: [PATCH 382/396] Fix a bug in the new index calculations --- certbot-nginx/certbot_nginx/nginxparser.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/certbot-nginx/certbot_nginx/nginxparser.py b/certbot-nginx/certbot_nginx/nginxparser.py index f65864df4..412268d22 100644 --- a/certbot-nginx/certbot_nginx/nginxparser.py +++ b/certbot-nginx/certbot_nginx/nginxparser.py @@ -235,15 +235,16 @@ class UnspacedList(list): def _spaced_position(self, idx): "Convert from indexes in the unspaced list to positions in the spaced one" - print "lookup", idx - spaces = 0 - pos = 0 + pos = spaces = 0 + # Normalize indexes like list[-1] etc, and save the result if idx < 0: idx = len(self) + idx + idx0 = idx + # Count the number of spaces in the spaced list before idx in the unspaced one while idx != -1: if spacey(self.spaced[pos]): spaces += 1 else: idx -= 1 pos += 1 - return idx + spaces + return idx0 + spaces From 6a938f2ee7ba48e891a783b0dd8880e0faa55883 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Fri, 24 Jun 2016 16:52:02 -0700 Subject: [PATCH 383/396] Fix docstring nit --- certbot-nginx/certbot_nginx/tests/nginxparser_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certbot-nginx/certbot_nginx/tests/nginxparser_test.py b/certbot-nginx/certbot_nginx/tests/nginxparser_test.py index 4c239ac10..1707bc6d6 100644 --- a/certbot-nginx/certbot_nginx/tests/nginxparser_test.py +++ b/certbot-nginx/certbot_nginx/tests/nginxparser_test.py @@ -176,7 +176,7 @@ class TestRawNginxParser(unittest.TestCase): ]) class TestUnspacedList(unittest.TestCase): - """Test the raw low-level Nginx config parser.""" + """Test the UnspacedList data structure""" def setUp(self): self.a = ["\n ", "things", " ", "quirk"] self.b = ["y", " "] From 880cb0319164d63ee6fed7a1e6cabcd4e46fd18c Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Fri, 24 Jun 2016 18:50:18 -0700 Subject: [PATCH 384/396] Make assertions about index policing --- certbot-nginx/certbot_nginx/tests/nginxparser_test.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/certbot-nginx/certbot_nginx/tests/nginxparser_test.py b/certbot-nginx/certbot_nginx/tests/nginxparser_test.py index 1707bc6d6..a0ef82cd5 100644 --- a/certbot-nginx/certbot_nginx/tests/nginxparser_test.py +++ b/certbot-nginx/certbot_nginx/tests/nginxparser_test.py @@ -219,6 +219,10 @@ class TestUnspacedList(unittest.TestCase): self.assertEqual(ul3, ["zither", ["zather", "zest"]]) self.assertEqual(ul3.spaced, [self.a[0], "zither", " ", l]) + def test_get(self): + self.assertRaises(IndexError, self.ul2.__getitem__, 2) + self.assertRaises(IndexError, self.ul2.__getitem__, -3) + def test_rawlists(self): ul3 = copy.deepcopy(self.ul) ul3.insert(0, "some") From 0dc4639cbffe023520c18af375d2e909cdab0137 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Fri, 24 Jun 2016 18:56:30 -0700 Subject: [PATCH 385/396] Be more explicit about range policing (rather than doing it in some roundabout crazy way) --- certbot-nginx/certbot_nginx/nginxparser.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/certbot-nginx/certbot_nginx/nginxparser.py b/certbot-nginx/certbot_nginx/nginxparser.py index 412268d22..1de079cab 100644 --- a/certbot-nginx/certbot_nginx/nginxparser.py +++ b/certbot-nginx/certbot_nginx/nginxparser.py @@ -239,6 +239,8 @@ class UnspacedList(list): # Normalize indexes like list[-1] etc, and save the result if idx < 0: idx = len(self) + idx + if not 0 <= idx < len(self): + raise IndexError("list index out of range") idx0 = idx # Count the number of spaces in the spaced list before idx in the unspaced one while idx != -1: From 98d261596c554e31346f4aec69fd8356167e851c Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Fri, 24 Jun 2016 19:03:46 -0700 Subject: [PATCH 386/396] Raise NotImplemented for all problematic list methods --- certbot-nginx/certbot_nginx/nginxparser.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/certbot-nginx/certbot_nginx/nginxparser.py b/certbot-nginx/certbot_nginx/nginxparser.py index 1de079cab..065858797 100644 --- a/certbot-nginx/certbot_nginx/nginxparser.py +++ b/certbot-nginx/certbot_nginx/nginxparser.py @@ -213,6 +213,16 @@ class UnspacedList(list): l.extend(other) return l + def index(self, _, _j=0, _k=0): + raise NotImplementedError("UnspacedList.index() not yet implemented") + def pop(self, _): + raise NotImplementedError("UnspacedList.pop() not yet implemented") + def remove(self, _): + raise NotImplementedError("UnspacedList.remove() not yet implemented") + def reverse(self, _): + raise NotImplementedError("UnspacedList.reverse() not yet implemented") + def sort(self, _cmp=None, _key=None, _Rev=None): + raise NotImplementedError("UnspacedList.sort() not yet implemented") def __setslice__(self, i, j, newslice): raise NotImplementedError("Slice operations on UnspacedLists not yet implemented") From f6069c2297364dbed088265745f9fd7ede7c4402 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Fri, 24 Jun 2016 18:23:09 -0700 Subject: [PATCH 387/396] Explain what is happening when the user cancels vhost selection --- certbot-apache/certbot_apache/obj.py | 4 ++++ certbot-apache/certbot_apache/tls_sni_01.py | 1 + 2 files changed, 5 insertions(+) diff --git a/certbot-apache/certbot_apache/obj.py b/certbot-apache/certbot_apache/obj.py index b88b22428..c1af352cb 100644 --- a/certbot-apache/certbot_apache/obj.py +++ b/certbot-apache/certbot_apache/obj.py @@ -6,6 +6,7 @@ from certbot.plugins import common class Addr(common.Addr): """Represents an Apache address.""" + def __eq__(self, other): """This is defined as equalivalent within Apache. @@ -18,6 +19,9 @@ class Addr(common.Addr): self.is_wildcard() and other.is_wildcard())) return False + def __repr__(self): + return "certbot_apache.obj.Addr(" + repr(self.tup) + ")" + def __ne__(self, other): return not self.__eq__(other) diff --git a/certbot-apache/certbot_apache/tls_sni_01.py b/certbot-apache/certbot_apache/tls_sni_01.py index f14f7be0f..187ecd279 100644 --- a/certbot-apache/certbot_apache/tls_sni_01.py +++ b/certbot-apache/certbot_apache/tls_sni_01.py @@ -129,6 +129,7 @@ class ApacheTlsSni01(common.TLSSNI01): # because it's a new vhost that's not configured yet (GH #677), # or perhaps because there were multiple sections # in the config file (GH #1042). See also GH #2600. + logger.warn("Attempting to fall back to default vhost %s...", default_addr) addrs.add(default_addr) return addrs From e0691ede2c25ed361bee9cd7ae4e76515e65f564 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Fri, 24 Jun 2016 18:43:29 -0700 Subject: [PATCH 388/396] Provide clear log messages when Apache tries a default vhost --- certbot-apache/certbot_apache/configurator.py | 9 ++++++--- certbot-apache/certbot_apache/display_ops.py | 9 +++++---- certbot-apache/certbot_apache/tls_sni_01.py | 4 ++-- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/certbot-apache/certbot_apache/configurator.py b/certbot-apache/certbot_apache/configurator.py index 9e404177a..c51778ce5 100644 --- a/certbot-apache/certbot_apache/configurator.py +++ b/certbot-apache/certbot_apache/configurator.py @@ -327,9 +327,12 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): vhost = display_ops.select_vhost(target_name, self.vhosts) if vhost is None: logger.error( - "No vhost exists with servername or alias of: %s. " - "No vhost was selected. Please specify servernames " - "in the Apache config", target_name) + "No vhost exists with servername or alias of: %s " + "(or it's in a file with multiple files, which Certbot " + "can't parse yet). " + "No vhost was selected. Please specify ServerName or ServerAlias " + "in the Apache config, or split vhosts into separate files.", + target_name) raise errors.PluginError("No vhost selected") elif temp: return vhost diff --git a/certbot-apache/certbot_apache/display_ops.py b/certbot-apache/certbot_apache/display_ops.py index c9359e7d3..6b2964426 100644 --- a/certbot-apache/certbot_apache/display_ops.py +++ b/certbot-apache/certbot_apache/display_ops.py @@ -87,10 +87,11 @@ def _vhost_menu(domain, vhosts): "vhosts are not yet supported)".format(domain, os.linesep), choices, help_label="More Info", ok_label="Select") except errors.MissingCommandlineFlag as e: - msg = ("Failed to run Apache plugin non-interactively{1}{0}{1}" - "(The best solution is to add ServerName or ServerAlias " - "entries to the VirtualHost directives of your apache " - "configuration files.)".format(e, os.linesep)) + msg = ("Encountered vhost ambiguity but unable to ask for user guidance in " + "non-interactive mode. Currently Certbot needs each vhost to be " + "in its own conf file, and may need vhosts to be explicitly " + "labelled with ServerName or ServerAlias directories.") + logger.warn(msg) raise errors.MissingCommandlineFlag(msg) return code, tag diff --git a/certbot-apache/certbot_apache/tls_sni_01.py b/certbot-apache/certbot_apache/tls_sni_01.py index 187ecd279..32529485b 100644 --- a/certbot-apache/certbot_apache/tls_sni_01.py +++ b/certbot-apache/certbot_apache/tls_sni_01.py @@ -124,12 +124,12 @@ class ApacheTlsSni01(common.TLSSNI01): try: vhost = self.configurator.choose_vhost(achall.domain, temp=True) - except (PluginError, MissingCommandlineFlag): + except (PluginError, MissingCommandlineFlag), e: # We couldn't find the virtualhost for this domain, possibly # because it's a new vhost that's not configured yet (GH #677), # or perhaps because there were multiple sections # in the config file (GH #1042). See also GH #2600. - logger.warn("Attempting to fall back to default vhost %s...", default_addr) + logger.warn("Falling back to default vhost %s...", default_addr) addrs.add(default_addr) return addrs From e93ace79cc58063f34cc665c96cbcaa951478ebd Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Sat, 25 Jun 2016 10:51:14 -0700 Subject: [PATCH 389/396] Test coverage & fix --- certbot-apache/certbot_apache/display_ops.py | 2 +- certbot-apache/certbot_apache/tests/display_ops_test.py | 2 +- certbot-apache/certbot_apache/tests/obj_test.py | 3 +++ certbot-apache/certbot_apache/tls_sni_01.py | 2 +- 4 files changed, 6 insertions(+), 3 deletions(-) diff --git a/certbot-apache/certbot_apache/display_ops.py b/certbot-apache/certbot_apache/display_ops.py index 6b2964426..24aabaed4 100644 --- a/certbot-apache/certbot_apache/display_ops.py +++ b/certbot-apache/certbot_apache/display_ops.py @@ -86,7 +86,7 @@ def _vhost_menu(domain, vhosts): "like to choose?\n(note: conf files with multiple " "vhosts are not yet supported)".format(domain, os.linesep), choices, help_label="More Info", ok_label="Select") - except errors.MissingCommandlineFlag as e: + except errors.MissingCommandlineFlag: msg = ("Encountered vhost ambiguity but unable to ask for user guidance in " "non-interactive mode. Currently Certbot needs each vhost to be " "in its own conf file, and may need vhosts to be explicitly " diff --git a/certbot-apache/certbot_apache/tests/display_ops_test.py b/certbot-apache/certbot_apache/tests/display_ops_test.py index fd1e52fde..585661c7f 100644 --- a/certbot-apache/certbot_apache/tests/display_ops_test.py +++ b/certbot-apache/certbot_apache/tests/display_ops_test.py @@ -38,7 +38,7 @@ class SelectVhostTest(unittest.TestCase): try: self._call(self.vhosts) except errors.MissingCommandlineFlag as e: - self.assertTrue("VirtualHost directives" in e.message) + self.assertTrue("vhost ambiguity" in e.message) @mock.patch("certbot_apache.display_ops.zope.component.getUtility") def test_more_info_cancel(self, mock_util): diff --git a/certbot-apache/certbot_apache/tests/obj_test.py b/certbot-apache/certbot_apache/tests/obj_test.py index 4c3d331be..10dba18bc 100644 --- a/certbot-apache/certbot_apache/tests/obj_test.py +++ b/certbot-apache/certbot_apache/tests/obj_test.py @@ -22,6 +22,9 @@ class VirtualHostTest(unittest.TestCase): self.vhost2 = VirtualHost( "fp", "vhp", set([self.addr2]), False, False, "localhost") + def test_repr(self): + self.assertEqual(repr(self.addr2), "certbot_apache.obj.Addr(('127.0.0.1', '443'))") + def test_eq(self): self.assertTrue(self.vhost1b == self.vhost1) self.assertFalse(self.vhost1 == self.vhost2) diff --git a/certbot-apache/certbot_apache/tls_sni_01.py b/certbot-apache/certbot_apache/tls_sni_01.py index 32529485b..06d9e9ebd 100644 --- a/certbot-apache/certbot_apache/tls_sni_01.py +++ b/certbot-apache/certbot_apache/tls_sni_01.py @@ -124,7 +124,7 @@ class ApacheTlsSni01(common.TLSSNI01): try: vhost = self.configurator.choose_vhost(achall.domain, temp=True) - except (PluginError, MissingCommandlineFlag), e: + except (PluginError, MissingCommandlineFlag): # We couldn't find the virtualhost for this domain, possibly # because it's a new vhost that's not configured yet (GH #677), # or perhaps because there were multiple sections From fdbb69930b53e8032dcb390b7c8c94e7f35408ec Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Mon, 27 Jun 2016 12:00:41 -0700 Subject: [PATCH 390/396] Tweak the NotImplementedError cases --- certbot-nginx/certbot_nginx/nginxparser.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/certbot-nginx/certbot_nginx/nginxparser.py b/certbot-nginx/certbot_nginx/nginxparser.py index 065858797..1f0569651 100644 --- a/certbot-nginx/certbot_nginx/nginxparser.py +++ b/certbot-nginx/certbot_nginx/nginxparser.py @@ -213,17 +213,15 @@ class UnspacedList(list): l.extend(other) return l - def index(self, _, _j=0, _k=0): - raise NotImplementedError("UnspacedList.index() not yet implemented") - def pop(self, _): + def pop(self, _i=0): raise NotImplementedError("UnspacedList.pop() not yet implemented") def remove(self, _): raise NotImplementedError("UnspacedList.remove() not yet implemented") - def reverse(self, _): + def reverse(self): raise NotImplementedError("UnspacedList.reverse() not yet implemented") def sort(self, _cmp=None, _key=None, _Rev=None): raise NotImplementedError("UnspacedList.sort() not yet implemented") - def __setslice__(self, i, j, newslice): + def __setslice__(self, _i, _j, _newslice): raise NotImplementedError("Slice operations on UnspacedLists not yet implemented") def __setitem__(self, i, value): From 184f54cbc799039276ad2c7eb17a307b363ca9ac Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Mon, 27 Jun 2016 12:36:28 -0700 Subject: [PATCH 391/396] cosmetic improvements --- certbot-nginx/certbot_nginx/nginxparser.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/certbot-nginx/certbot_nginx/nginxparser.py b/certbot-nginx/certbot_nginx/nginxparser.py index 1f0569651..ccf680dfc 100644 --- a/certbot-nginx/certbot_nginx/nginxparser.py +++ b/certbot-nginx/certbot_nginx/nginxparser.py @@ -41,7 +41,8 @@ class RawNginxParser(object): block = Forward() block << Group( - # key could for instance be "server" or "http" + # key could for instance be "server" or "http", or "location" (in which case + # location_statement needs to have a non-empty location) (Group(space + key + location_statement) ^ Group(if_statement) ^ Group(map_statement)).leaveWhitespace() + left_bracket + @@ -213,7 +214,7 @@ class UnspacedList(list): l.extend(other) return l - def pop(self, _i=0): + def pop(self, _i=None): raise NotImplementedError("UnspacedList.pop() not yet implemented") def remove(self, _): raise NotImplementedError("UnspacedList.remove() not yet implemented") From 6017a6cb6daca4a0eddd7a85b874f66e26d9d78c Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Fri, 24 Jun 2016 17:14:14 -0700 Subject: [PATCH 392/396] Only write nginx config files if we've modified them --- certbot-nginx/certbot_nginx/nginxparser.py | 17 +++++++++++++++-- certbot-nginx/certbot_nginx/parser.py | 5 ++++- .../certbot_nginx/tests/parser_test.py | 2 +- 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/certbot-nginx/certbot_nginx/nginxparser.py b/certbot-nginx/certbot_nginx/nginxparser.py index ccf680dfc..816507738 100644 --- a/certbot-nginx/certbot_nginx/nginxparser.py +++ b/certbot-nginx/certbot_nginx/nginxparser.py @@ -163,6 +163,7 @@ class UnspacedList(list): def __init__(self, list_source): # ensure our argument is not a generator, and duplicate any sublists self.spaced = copy.deepcopy(list(list_source)) + self.dirty = False # Turn self into a version of the source list that has spaces removed # and all sub-lists also UnspacedList()ed @@ -198,20 +199,24 @@ class UnspacedList(list): item, spaced_item = self._coerce(x) self.spaced.insert(self._spaced_position(i), spaced_item) list.insert(self, i, item) + self.dirty = True def append(self, x): item, spaced_item = self._coerce(x) self.spaced.append(spaced_item) list.append(self, item) + self.dirty = True def extend(self, x): item, spaced_item = self._coerce(x) self.spaced.extend(spaced_item) list.extend(self, item) + self.dirty = True def __add__(self, other): l = copy.deepcopy(self) l.extend(other) + l.dirty = True return l def pop(self, _i=None): @@ -231,16 +236,24 @@ class UnspacedList(list): item, spaced_item = self._coerce(value) self.spaced.__setitem__(self._spaced_position(i), spaced_item) list.__setitem__(self, i, item) + self.dirty = True def __delitem__(self, i): self.spaced.__delitem__(self._spaced_position(i)) list.__delitem__(self, i) + self.dirty = True - def __deepcopy__(self, unused_memo): + def __deepcopy__(self, memo): l = UnspacedList(self[:]) - l.spaced = copy.deepcopy(self.spaced) + l.spaced = copy.deepcopy(self.spaced, memo=memo) + l.dirty = self.dirty return l + def is_dirty(self): + """Recurse through the parse tree to figure out if any sublists are dirty""" + if self.dirty: + return True + return any((isinstance(x, list) and x.is_dirty() for x in self)) def _spaced_position(self, idx): "Convert from indexes in the unspaced list to positions in the spaced one" diff --git a/certbot-nginx/certbot_nginx/parser.py b/certbot-nginx/certbot_nginx/parser.py index f2c10ab70..e7992810a 100644 --- a/certbot-nginx/certbot_nginx/parser.py +++ b/certbot-nginx/certbot_nginx/parser.py @@ -205,11 +205,12 @@ class NginxParser(object): raise errors.NoInstallationError( "Could not find configuration root") - def filedump(self, ext='tmp'): + def filedump(self, ext='tmp', lazy=True): """Dumps parsed configurations into files. :param str ext: The file extension to use for the dumped files. If empty, this overrides the existing conf files. + :param bool lazy: Only write files that have been modified """ # Best-effort atomicity is enforced above us by reverter.py @@ -218,6 +219,8 @@ class NginxParser(object): if ext: filename = filename + os.path.extsep + ext try: + if lazy and not tree.is_dirty(): + continue out = nginxparser.dumps(tree) logger.debug('Writing nginx conf tree to %s:\n%s', filename, out) with open(filename, 'w') as _file: diff --git a/certbot-nginx/certbot_nginx/tests/parser_test.py b/certbot-nginx/certbot_nginx/tests/parser_test.py index 6d046178a..464d717ed 100644 --- a/certbot-nginx/certbot_nginx/tests/parser_test.py +++ b/certbot-nginx/certbot_nginx/tests/parser_test.py @@ -66,7 +66,7 @@ class NginxParserTest(util.NginxTest): def test_filedump(self): nparser = parser.NginxParser(self.config_path, self.ssl_options) - nparser.filedump('test') + nparser.filedump('test', lazy=False) # pylint: disable=protected-access parsed = nparser._parse_files(nparser.abs_path( 'sites-enabled/example.com.test')) From 3e9bf2f35f6fc91e7b56109d3ee81685970b2271 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Thu, 23 Jun 2016 14:03:28 -0700 Subject: [PATCH 393/396] Tests for UnspacedList.is_dirty() --- certbot-nginx/certbot_nginx/tests/nginxparser_test.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/certbot-nginx/certbot_nginx/tests/nginxparser_test.py b/certbot-nginx/certbot_nginx/tests/nginxparser_test.py index a0ef82cd5..a0a0736a5 100644 --- a/certbot-nginx/certbot_nginx/tests/nginxparser_test.py +++ b/certbot-nginx/certbot_nginx/tests/nginxparser_test.py @@ -231,6 +231,17 @@ class TestUnspacedList(unittest.TestCase): del ul3[2] self.assertEqual(ul3, ["some", "things", "why", "did", "whether"]) + def test_is_dirty(self): + self.assertEqual(False, self.ul2.is_dirty()) + ul3 = UnspacedList([]) + ul3.append(self.ul) + self.assertEqual(False, self.ul.is_dirty()) + self.assertEqual(True, ul3.is_dirty()) + ul4 = UnspacedList([[1], [2, 3, 4]]) + self.assertEqual(False, ul4.is_dirty()) + ul4[1][2] = 5 + self.assertEqual(True, ul4.is_dirty()) + if __name__ == '__main__': unittest.main() # pragma: no cover From bdbdd826de3ec8cecf3834e7b44848faa93b3721 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Mon, 27 Jun 2016 12:44:53 -0700 Subject: [PATCH 394/396] Extra comment & concision --- certbot-nginx/certbot_nginx/nginxparser.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/certbot-nginx/certbot_nginx/nginxparser.py b/certbot-nginx/certbot_nginx/nginxparser.py index 816507738..d6c352296 100644 --- a/certbot-nginx/certbot_nginx/nginxparser.py +++ b/certbot-nginx/certbot_nginx/nginxparser.py @@ -1,4 +1,5 @@ """Very low-level nginx config parser based on pyparsing.""" +# Forked from https://github.com/fatiherikli/nginxparser (MIT Licensed) import copy import logging import string @@ -81,8 +82,7 @@ class RawNginxDumper(object): yield b.pop(0) # indentation if not b: continue - key = b.pop(0) - values = b.pop(0) + key, values = b.pop(0), b.pop(0) if isinstance(key, list): yield "".join(key) + '{' @@ -91,9 +91,9 @@ class RawNginxDumper(object): yield line yield '}' else: - if isinstance(key, str) and key.strip() == '#': + if isinstance(key, str) and key.strip() == '#': # comment yield key + values - else: + else: # assignment gap = "" # Sometimes the parser has stuck some gap whitespace in here; # if so rotate it into gap From 7b50960ac0559e4f2e9fea20a72470127b264515 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Wed, 29 Jun 2016 13:57:58 -0700 Subject: [PATCH 395/396] Address review comments --- certbot-apache/certbot_apache/configurator.py | 2 +- certbot-apache/certbot_apache/obj.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/certbot-apache/certbot_apache/configurator.py b/certbot-apache/certbot_apache/configurator.py index c51778ce5..89d602f5f 100644 --- a/certbot-apache/certbot_apache/configurator.py +++ b/certbot-apache/certbot_apache/configurator.py @@ -328,7 +328,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): if vhost is None: logger.error( "No vhost exists with servername or alias of: %s " - "(or it's in a file with multiple files, which Certbot " + "(or it's in a file with multiple vhosts, which Certbot " "can't parse yet). " "No vhost was selected. Please specify ServerName or ServerAlias " "in the Apache config, or split vhosts into separate files.", diff --git a/certbot-apache/certbot_apache/obj.py b/certbot-apache/certbot_apache/obj.py index c1af352cb..c71443e92 100644 --- a/certbot-apache/certbot_apache/obj.py +++ b/certbot-apache/certbot_apache/obj.py @@ -19,12 +19,12 @@ class Addr(common.Addr): self.is_wildcard() and other.is_wildcard())) return False - def __repr__(self): - return "certbot_apache.obj.Addr(" + repr(self.tup) + ")" - def __ne__(self, other): return not self.__eq__(other) + def __repr__(self): + return "certbot_apache.obj.Addr(" + repr(self.tup) + ")" + def _addr_less_specific(self, addr): """Returns if addr.get_addr() is more specific than self.get_addr().""" # pylint: disable=protected-access From a70cf6fcf80c3c3322033d0f69f0802df91a4c9f Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 29 Jun 2016 15:22:19 -0700 Subject: [PATCH 396/396] Use correct port name --- docs/using.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/using.rst b/docs/using.rst index 4e49c3eb5..fb96bb853 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -428,7 +428,7 @@ Operating System Packages **FreeBSD** - * Port: ``cd /usr/ports/security/py-letsencrypt && make install clean`` + * Port: ``cd /usr/ports/security/py-certbot && make install clean`` * Package: ``pkg install py27-letsencrypt`` **OpenBSD**