diff options
98 files changed, 1795 insertions, 1010 deletions
diff --git a/.travis.yml b/.travis.yml index de17c0402..3edf9b167 100644 --- a/.travis.yml +++ b/.travis.yml @@ -169,8 +169,7 @@ notifications: email: false irc: channels: - - "chat.freenode.net#letsencrypt" + - "chat.freenode.net#letsencrypt-dev" on_success: never on_failure: always use_notice: true - skip_join: true diff --git a/CHANGELOG.md b/CHANGELOG.md index a0f196ec7..377c472f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,87 @@ Certbot adheres to [Semantic Versioning](http://semver.org/). +## 0.17.0 - 2017-08-02 + +### Added + +* Support in our nginx plugin for modifying SSL server blocks that do + not contain certificate or key directives. +* A `--max-log-backups` flag to allow users to configure or even completely + disable Certbot's built in log rotation. +* A `--user-agent-comment` flag to allow people who build tools around Certbot + to differentiate their user agent string by adding a comment to its default + value. + +### Changed + +* Due to some awesome work by + [cryptography project](https://github.com/pyca/cryptography), compilation can + now be avoided on most systems when using certbot-auto. This eliminates many + problems people have had in the past such as running out of memory, having + invalid headers/libraries, and changes to the OS packages on their system + after compilation breaking Certbot. +* The `--renew-hook` flag has been hidden in favor of `--deploy-hook`. This new + flag works exactly the same way except it is always run when a certificate is + issued rather than just when it is renewed. +* We have started printing deprecation warnings in certbot-auto for + experimentally supported systems with OS packages available. +* A certificate lineage's name is included in error messages during renewal. + +### Fixed + +* Encoding errors that could occur when parsing error messages from the ACME + server containing Unicode have been resolved. +* certbot-auto no longer prints misleading messages about there being a newer + pip version available when installation fails. +* Certbot's ACME library now properly extracts domains from critical SAN + extensions. + +More details about these changes can be found on our GitHub repo: +https://github.com/certbot/certbot/issues?q=is%3Aissue+milestone%3A0.17.0+is%3Aclosed + +## 0.16.0 - 2017-07-05 + +### Added + +* A plugin for performing DNS challenges using dynamic DNS updates as defined + in RFC 2316. This plugin is packaged separately from Certbot and is available + at https://pypi.python.org/pypi/certbot-dns-rfc2136. It supports Python 2.6, + 2.7, and 3.3+. At this time, there isn't a good way to install this plugin + when using certbot-auto, but this should change in the near future. +* Plugins for performing DNS challenges for the providers + [DNS Made Easy](https://pypi.python.org/pypi/certbot-dns-dnsmadeeasy) and + [LuaDNS](https://pypi.python.org/pypi/certbot-dns-luadns). These plugins are + packaged separately from Certbot and support Python 2.7 and 3.3+. Currently, + there isn't a good way to install these plugins when using certbot-auto, + but that should change soon. +* Support for performing TLS-SNI-01 challenges when using the manual plugin. +* Automatic detection of Arch Linux in the Apache plugin providing better + default settings for the plugin. + +### Changed + +* The text of the interactive question about whether a redirect from HTTP to + HTTPS should be added by Certbot has been rewritten to better explain the + choices to the user. +* Simplified HTTP challenge instructions in the manual plugin. + +### Fixed + +* Problems performing a dry run when using the Nginx plugin have been fixed. +* Resolved an issue where certbot-dns-digitalocean's test suite would sometimes + fail when ran using Python 3. +* On some systems, previous versions of certbot-auto would error out with a + message about a missing hash for setuptools. This has been fixed. +* A bug where Certbot would sometimes not print a space at the end of an + interactive prompt has been resolved. +* Nonfatal tracebacks are no longer shown in rare cases where Certbot + encounters an exception trying to close its TCP connection with the ACME + server. + +More details about these changes can be found on our GitHub repo: +https://github.com/certbot/certbot/issues?q=is%3Aissue+milestone%3A0.16.0+is%3Aclosed + ## 0.15.0 - 2017-06-08 ### Added diff --git a/acme/acme/client.py b/acme/acme/client.py index 9455159de..fa903f0e6 100644 --- a/acme/acme/client.py +++ b/acme/acme/client.py @@ -519,7 +519,12 @@ class ClientNetwork(object): # pylint: disable=too-many-instance-attributes self._default_timeout = timeout def __del__(self): - self.session.close() + # Try to close the session, but don't show exceptions to the + # user if the call to close() fails. See #4840. + try: + self.session.close() + except Exception: # pylint: disable=broad-except + pass def _wrap_in_jws(self, obj, nonce): """Wrap `JSONDeSerializable` object in JWS. diff --git a/acme/acme/client_test.py b/acme/acme/client_test.py index cd1a90645..54652b46c 100644 --- a/acme/acme/client_test.py +++ b/acme/acme/client_test.py @@ -600,12 +600,19 @@ class ClientNetworkTest(unittest.TestCase): mock.ANY, mock.ANY, verify=mock.ANY, headers=mock.ANY, timeout=45) - def test_del(self): + def test_del(self, close_exception=None): sess = mock.MagicMock() + + if close_exception is not None: + sess.close.side_effect = close_exception + self.net.session = sess del self.net sess.close.assert_called_once_with() + def test_del_error(self): + self.test_del(ReferenceError) + @mock.patch('acme.client.requests') def test_requests_error_passthrough(self, mock_requests): mock_requests.exceptions = requests.exceptions diff --git a/acme/acme/crypto_util.py b/acme/acme/crypto_util.py index 84b70e4a6..de15284c0 100644 --- a/acme/acme/crypto_util.py +++ b/acme/acme/crypto_util.py @@ -218,7 +218,7 @@ def _pyopenssl_cert_or_req_san(cert_or_req): text = func(OpenSSL.crypto.FILETYPE_TEXT, cert_or_req).decode("utf-8") # WARNING: this function does not support multiple SANs extensions. # Multiple X509v3 extensions of the same type is disallowed by RFC 5280. - match = re.search(r"X509v3 Subject Alternative Name:\s*(.*)", text) + match = re.search(r"X509v3 Subject Alternative Name:(?: critical)?\s*(.*)", text) # WARNING: this function assumes that no SAN can include # parts_separator, hence the split! sans_parts = [] if match is None else match.group(1).split(parts_separator) diff --git a/acme/acme/crypto_util_test.py b/acme/acme/crypto_util_test.py index 845f43914..4046aa197 100644 --- a/acme/acme/crypto_util_test.py +++ b/acme/acme/crypto_util_test.py @@ -131,6 +131,11 @@ class PyOpenSSLCertOrReqSANTest(unittest.TestCase): self.assertEqual(self._call_csr('csr-idnsans.pem'), self._get_idn_names()) + def test_critical_san(self): + self.assertEqual(self._call_cert('critical-san.pem'), + ['chicago-cubs.venafi.example', 'cubs.venafi.example']) + + class RandomSnTest(unittest.TestCase): """Test for random certificate serial numbers.""" diff --git a/acme/acme/messages.py b/acme/acme/messages.py index 5784e8e11..4b4fa5003 100644 --- a/acme/acme/messages.py +++ b/acme/acme/messages.py @@ -1,5 +1,6 @@ """ACME protocol messages.""" import collections +import six from acme import challenges from acme import errors @@ -36,9 +37,13 @@ ERROR_TYPE_DESCRIPTIONS.update(dict( # add errors with old prefix, deprecate me def is_acme_error(err): """Check if argument is an ACME error.""" - return (ERROR_PREFIX in str(err)) or (OLD_ERROR_PREFIX in str(err)) + if isinstance(err, Error) and (err.typ is not None): + return (ERROR_PREFIX in err.typ) or (OLD_ERROR_PREFIX in err.typ) + else: + return False +@six.python_2_unicode_compatible class Error(jose.JSONObjectWithFields, errors.Error): """ACME error. @@ -92,10 +97,10 @@ class Error(jose.JSONObjectWithFields, errors.Error): return code def __str__(self): - return ' :: '.join( - part for part in + return b' :: '.join( + part.encode('ascii', 'backslashreplace') for part in (self.typ, self.description, self.detail, self.title) - if part is not None) + if part is not None).decode() class _Constant(jose.JSONDeSerializable, collections.Hashable): # type: ignore diff --git a/acme/acme/messages_test.py b/acme/acme/messages_test.py index ab05a89b7..b4ce19a08 100644 --- a/acme/acme/messages_test.py +++ b/acme/acme/messages_test.py @@ -26,6 +26,7 @@ class ErrorTest(unittest.TestCase): 'type': ERROR_PREFIX + 'malformed', } self.error_custom = Error(typ='custom', detail='bar') + self.empty_error = Error() self.jobj_custom = {'type': 'custom', 'detail': 'bar'} def test_default_typ(self): @@ -45,12 +46,6 @@ class ErrorTest(unittest.TestCase): 'The request message was malformed', self.error.description) self.assertTrue(self.error_custom.description is None) - def test_str(self): - self.assertEqual( - 'urn:ietf:params:acme:error:malformed :: The request message was ' - 'malformed :: foo :: title', str(self.error)) - self.assertEqual('custom :: bar', str(self.error_custom)) - def test_code(self): from acme.messages import Error self.assertEqual('malformed', self.error.code) @@ -60,8 +55,16 @@ class ErrorTest(unittest.TestCase): def test_is_acme_error(self): from acme.messages import is_acme_error self.assertTrue(is_acme_error(self.error)) - self.assertTrue(is_acme_error(str(self.error))) self.assertFalse(is_acme_error(self.error_custom)) + self.assertFalse(is_acme_error(self.empty_error)) + self.assertFalse(is_acme_error("must pet all the {dogs|rabbits}")) + + def test_unicode_error(self): + from acme.messages import Error, ERROR_PREFIX, is_acme_error + arabic_error = Error( + detail=u'\u0639\u062f\u0627\u0644\u0629', typ=ERROR_PREFIX + 'malformed', + title='title') + self.assertTrue(is_acme_error(arabic_error)) def test_with_code(self): from acme.messages import Error, is_acme_error diff --git a/acme/acme/testdata/critical-san.pem b/acme/acme/testdata/critical-san.pem new file mode 100644 index 000000000..7aec8ab1c --- /dev/null +++ b/acme/acme/testdata/critical-san.pem @@ -0,0 +1,28 @@ +-----BEGIN CERTIFICATE----- +MIIErTCCA5WgAwIBAgIKETb7VQAAAAAdGTANBgkqhkiG9w0BAQsFADCBkTELMAkG +A1UEBhMCVVMxDTALBgNVBAgTBFV0YWgxFzAVBgNVBAcTDlNhbHQgTGFrZSBDaXR5 +MRUwEwYDVQQKEwxWZW5hZmksIEluYy4xHzAdBgNVBAsTFkRlbW9uc3RyYXRpb24g +U2VydmljZXMxIjAgBgNVBAMTGVZlbmFmaSBFeGFtcGxlIElzc3VpbmcgQ0EwHhcN +MTcwNzEwMjMxNjA1WhcNMTcwODA5MjMxNjA1WjAAMIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEA7CU5qRIzCs9hCRiSUvLZ8r81l4zIYbx1V1vZz6x1cS4M +0keNfFJ1wB+zuvx80KaMYkWPYlg4Rsm9Ok3ZapakXDlaWtrfg78lxtHuPw1o7AYV +EXDwwPkNugLMJfYw5hWYSr8PCLcOJoY00YQ0fJ44L+kVsUyGjN4UTRRZmOh/yNVU +0W12dTCz4X7BAW01OuY6SxxwewnW3sBEep+APfr2jd/oIx7fgZmVB8aRCDPj4AFl +XINWIwxmptOwnKPbwLN/vhCvJRUkO6rA8lpYwQkedFf6fHhqi2Sq/NCEOg4RvMCF +fKbMpncOXxz+f4/i43SVLrPz/UyhjNbKGJZ+zFrQowIDAQABo4IBlTCCAZEwPgYD +VR0RAQH/BDQwMoIbY2hpY2Fnby1jdWJzLnZlbmFmaS5leGFtcGxlghNjdWJzLnZl +bmFmaS5leGFtcGxlMB0GA1UdDgQWBBTgKZXVSFNyPHHtO/phtIALPcCF5DAfBgNV +HSMEGDAWgBT/JJ6Wei/pzf+9DRHuv6Wgdk2HsjBSBgNVHR8ESzBJMEegRaBDhkFo +dHRwOi8vcGtpLnZlbmFmaS5leGFtcGxlL2NybC9WZW5hZmklMjBFeGFtcGxlJTIw +SXNzdWluZyUyMENBLmNybDA6BggrBgEFBQcBAQQuMCwwKgYIKwYBBQUHMAGGHmh0 +dHA6Ly9wa2kudmVuYWZpLmV4YW1wbGUvb2NzcDAOBgNVHQ8BAf8EBAMCBaAwPQYJ +KwYBBAGCNxUHBDAwLgYmKwYBBAGCNxUIhIDLGYTvsSSEnZ8ehvD5UofP4hMEgobv +DIGy4mcCAWQCAQIwEwYDVR0lBAwwCgYIKwYBBQUHAwEwGwYJKwYBBAGCNxUKBA4w +DDAKBggrBgEFBQcDATANBgkqhkiG9w0BAQsFAAOCAQEA3YW4t1AzxEn384OqdU6L +ny8XkMhWpRM0W0Z9ZC3gRZKbVUu49nG/KB5hbVn/de33zdX9HOZJKc0vXzkGZQUs +OUCCsKX4VKzV5naGXOuGRbvV4CJh5P0kPlDzyb5t312S49nJdcdBf0Y/uL5Qzhst +bXy8qNfFNG3SIKKRAUpqE9OVIl+F+JBwexa+v/4dFtUOqMipfXxB3TaxnDqvU1dS +yO34ZTvIMGXJIZ5nn/d/LNc3N3vBg2SHkMpladqw0Hr7mL0bFOe0b+lJgkDP06Be +n08fikhz1j2AW4/ZHa9w4DUz7J21+RtHMhh+Vd1On0EAeZ563svDe7Z+yrg6zOVv +KA== +-----END CERTIFICATE-----
\ No newline at end of file diff --git a/acme/setup.py b/acme/setup.py index 4e6eaf50c..dad845c04 100644 --- a/acme/setup.py +++ b/acme/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.16.0.dev0' +version = '0.18.0.dev0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot-apache/certbot_apache/configurator.py b/certbot-apache/certbot_apache/configurator.py index 097c4ff42..9e3eb4139 100644 --- a/certbot-apache/certbot_apache/configurator.py +++ b/certbot-apache/certbot_apache/configurator.py @@ -347,9 +347,7 @@ 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 " - "(or it's in a file with multiple vhosts, which Certbot " - "can't parse yet). " + "No vhost exists with servername or alias of %s. " "No vhost was selected. Please specify ServerName or ServerAlias " "in the Apache config, or split vhosts into separate files.", target_name) diff --git a/certbot-apache/certbot_apache/display_ops.py b/certbot-apache/certbot_apache/display_ops.py index f9e0802f7..7aec26f81 100644 --- a/certbot-apache/certbot_apache/display_ops.py +++ b/certbot-apache/certbot_apache/display_ops.py @@ -85,10 +85,11 @@ def _vhost_menu(domain, vhosts): "vhosts are not yet supported)".format(domain, os.linesep), choices, force_interactive=True) 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 " - "labelled with ServerName or ServerAlias directives.") + msg = ( + "Encountered vhost ambiguity but unable to ask for user " + "guidance in non-interactive mode. Certbot may need " + "vhosts to be explicitly labelled with ServerName or " + "ServerAlias directives.") logger.warning(msg) raise errors.MissingCommandlineFlag(msg) diff --git a/certbot-apache/certbot_apache/tls_sni_01.py b/certbot-apache/certbot_apache/tls_sni_01.py index 65a66d2fd..da64400b1 100644 --- a/certbot-apache/certbot_apache/tls_sni_01.py +++ b/certbot-apache/certbot_apache/tls_sni_01.py @@ -126,9 +126,8 @@ class ApacheTlsSni01(common.TLSSNI01): vhost = self.configurator.choose_vhost(achall.domain, temp=True) 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 <VirtualHost> sections - # in the config file (GH #1042). See also GH #2600. + # because it's a new vhost that's not configured yet + # (GH #677). See also GH #2600. logger.warning("Falling back to default vhost %s...", default_addr) addrs.add(default_addr) return addrs diff --git a/certbot-apache/setup.py b/certbot-apache/setup.py index 64fd8be66..ea0085dfc 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.16.0.dev0' +version = '0.18.0.dev0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot-auto b/certbot-auto index 725c86895..b935ed447 100755 --- a/certbot-auto +++ b/certbot-auto @@ -28,7 +28,7 @@ if [ -z "$VENV_PATH" ]; then VENV_PATH="$XDG_DATA_HOME/$VENV_NAME" fi VENV_BIN="$VENV_PATH/bin" -LE_AUTO_VERSION="0.15.0" +LE_AUTO_VERSION="0.17.0" BASENAME=$(basename $0) USAGE="Usage: $BASENAME [OPTIONS] A self-updating wrapper script for the Certbot ACME client. When run, updates @@ -200,6 +200,25 @@ ExperimentalBootstrap() { fi } +DeprecationBootstrap() { + # Arguments: Platform name, bootstrap function name + if [ "$DEBUG" = 1 ]; then + if [ "$2" != "" ]; then + BootstrapMessage $1 + $2 + fi + else + error "WARNING: certbot-auto support for this $1 is DEPRECATED!" + error "Please visit certbot.eff.org to learn how to download a version of" + error "Certbot that is packaged for your system. While an existing version" + error "of certbot-auto may work currently, we have stopped supporting updating" + error "system packages for your system. Please switch to a packaged version" + error "as soon as possible." + 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. @@ -630,11 +649,11 @@ Bootstrap() { elif [ -f /etc/manjaro-release ]; then ExperimentalBootstrap "Manjaro Linux" BootstrapArchCommon elif [ -f /etc/gentoo-release ]; then - ExperimentalBootstrap "Gentoo" BootstrapGentooCommon + DeprecationBootstrap "Gentoo" BootstrapGentooCommon elif uname | grep -iq FreeBSD ; then - ExperimentalBootstrap "FreeBSD" BootstrapFreeBsd + DeprecationBootstrap "FreeBSD" BootstrapFreeBsd elif uname | grep -iq Darwin ; then - ExperimentalBootstrap "macOS" BootstrapMac + DeprecationBootstrap "macOS" 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 @@ -710,54 +729,78 @@ pycparser==2.14 \ asn1crypto==0.22.0 \ --hash=sha256:d232509fefcfcdb9a331f37e9c9dc20441019ad927c7d2176cf18ed5da0ba097 \ --hash=sha256:cbbadd640d3165ab24b06ef25d1dca09a3441611ac15f6a6b452474fdf0aed1a -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 +cffi==1.10.0 \ + --hash=sha256:446699c10f3c390633d0722bc19edbc7ac4b94761918a4a4f7908a24e86ebbd0 \ + --hash=sha256:562326fc7f55a59ef3fef5e82908fe938cdc4bbda32d734c424c7cd9ed73e93a \ + --hash=sha256:7f732ad4a30db0b39400c3f7011249f7d0701007d511bf09604729aea222871f \ + --hash=sha256:94fb8410c6c4fc48e7ea759d3d1d9ca561171a88d00faddd4aa0306f698ad6a0 \ + --hash=sha256:587a5043df4b00a2130e09fed42da02a4ed3c688bd9bf07a3ac89d2271f4fb07 \ + --hash=sha256:ec08b88bef627ec1cea210e1608c85d3cf44893bcde74e41b7f7dbdfd2c1bad6 \ + --hash=sha256:a41406f6d62abcdf3eef9fd998d8dcff04fd2a7746644143045feeebd76352d1 \ + --hash=sha256:b560916546b2f209d74b82bdbc3223cee9a165b0242fa00a06dfc48a2054864a \ + --hash=sha256:e74896774e437f4715c57edeb5cf3d3a40d7727f541c2c12156617b5a15d1829 \ + --hash=sha256:9a31c18ba4881a116e448c52f3f5d3e14401cf7a9c43cc88f06f2a7f5428da0e \ + --hash=sha256:80796ea68e11624a0279d3b802f88a7fe7214122b97a15a6c97189934a2cc776 \ + --hash=sha256:f4019826a2dec066c909a1f483ef0dcf9325d6740cc0bd15308942b28b0930f7 \ + --hash=sha256:7248506981eeba23888b4140a69a53c4c0c0a386abcdca61ed8dd790a73e64b9 \ + --hash=sha256:a8955265d146e86fe2ce116394be4eaf0cb40314a79b19f11c4fa574cd639572 \ + --hash=sha256:c49187260043bd4c1d6a52186f9774f17d9b1da0a406798ebf4bfc12da166ade \ + --hash=sha256:c1d8b3d8dcb5c23ac1a8bf56422036f3f305a3c5a8bc8c354256579a1e2aa2c1 \ + --hash=sha256:9e389615bcecb8c782a87939d752340bb0a3a097e90bae54d7f0915bc12f45bd \ + --hash=sha256:d09ff358f75a874f69fa7d1c2b4acecf4282a950293fcfcf89aa606da8a9a500 \ + --hash=sha256:b69b4557aae7de18b7c174a917fe19873529d927ac592762d9771661875bbd40 \ + --hash=sha256:5de52b081a2775e76b971de9d997d85c4457fc0a09079e12d66849548ae60981 \ + --hash=sha256:e7d88fecb7b6250a1fd432e6dc64890342c372fce13dbfe4bb6f16348ad00c14 \ + --hash=sha256:1426e67e855ef7f5030c9184f4f1a9f4bfa020c31c962cd41fd129ec5aef4a6a \ + --hash=sha256:267dd2c66a5760c5f4d47e2ebcf8eeac7ef01e1ae6ae7a6d0d241a290068bc38 \ + --hash=sha256:e553eb489511cacf19eda6e52bc9e151316f0d721724997dda2c4d3079b778db \ + --hash=sha256:98b89b2c57f97ce2db7aeba60db173c84871d73b40e41a11ea95de1500ddc57e \ + --hash=sha256:e2b7e090188833bc58b2ae03fb864c22688654ebd2096bcf38bc860c4f38a3d8 \ + --hash=sha256:afa7d8b8d38ad40db8713ee053d41b36d87d6ae5ec5ad36f9210b548a18dc214 \ + --hash=sha256:4fc9c2ff7924b3a1fa326e1799e5dd58cac585d7fb25fe53ccaa1333b0453d65 \ + --hash=sha256:937db39a1ec5af3003b16357b2042bba67c88d43bc11aaa203fa8a5924524209 \ + --hash=sha256:ab22285797631df3b513b2cd3ecdc51cd8e3d36788e3991d93d0759d6883b027 \ + --hash=sha256:96e599b924ef009aa867f725b3249ee51d76489f484d3a45b4bd219c5ec6ed59 \ + --hash=sha256:bea842a0512be6a8007e585790bccd5d530520fc025ce63b03e139be373b0063 \ + --hash=sha256:e7175287f7fe7b1cc203bb958b17db40abd732690c1e18e700f10e0843a58598 \ + --hash=sha256:285ab352552f52f1398c912556d4d36d4ea9b8450e5c65d03809bf9886755533 \ + --hash=sha256:5576644b859197da7bbd8f8c7c2fb5dcc6cd505cadb42992d5f104c013f8a214 \ + --hash=sha256:b3b02911eb1f6ada203b0763ba924234629b51586f72a21faacc638269f4ced5 ConfigArgParse==0.10.0 \ --hash=sha256:3b50a83dd58149dfcee98cb6565265d10b53e9c0a2bca7eeef7fb5f5524890a7 configobj==5.0.6 \ --hash=sha256:a2f5650770e1c87fb335af19a9b7eb73fc05ccf22144eb68db7d00cd2bcb0902 -cryptography==1.8.2 \ - --hash=sha256:e572527dc4eae300d4ac58c3a49fd0fe1a0178baf341f536d22c45455c958410 \ - --hash=sha256:15448bcfc4ef0f58c8e049f06cb10c296d75456ced02466dff3c82cc9c85f0a6 \ - --hash=sha256:771171a4b7677ee791f74928030fb59ca83a1710d32eaec8395c5170fc520741 \ - --hash=sha256:06e47faef940bc53ca23f5c6a29f5d4ebc47f0c7632f356da8ce4cc3ae99e908 \ - --hash=sha256:730a4f2c028b33b3e6b1a3caa7a3048a1e1e6ff2fe9043acdb21b31a2e711742 \ - --hash=sha256:1bf2299033c5a517014ffd5ec2e267f6a220d9095e75dd002dc33a898a7857cc \ - --hash=sha256:5e7249bc0360d834b89631e83c1a7bbb28098b59fab9816e5e19efdef7b71a1c \ - --hash=sha256:3971bdb5054257c922d95839d10ad347dcaa7137efeed34ce33ee660ea52b7e2 \ - --hash=sha256:a8b431f82972cec974766a484eba02d7bbf6a5c042c13c25f1a23d4a3a31bfb4 \ - --hash=sha256:a9281f0292131747f94219793438d78823bb72fbcafd1b415e99af1d8c42e11c \ - --hash=sha256:502237c0ed9ca77212cf1673627fd2c361ee989cdde2ac54a0cd3f17cbc79e5a \ - --hash=sha256:c63625ec36d1615fff69b068a95ea038d5f8df961901a097dfedc7e7410794d5 \ - --hash=sha256:bda0a32d99ee1f86fcd46bbb10f43216f101df3349187ea8999967cddbfede86 \ - --hash=sha256:a4a69088671eb31aa292a5996d9dd7a4ccb585b6fc7eb7b9e47051e1169fc479 \ - --hash=sha256:0f7127b0034d5112b190de6bf46fadc41940983a91836acfdaa16c44f44beb75 \ - --hash=sha256:9049c073f86155dfcd93434d63db80f753cd2e5bebf1d6172b112de215369b07 \ - --hash=sha256:264dd80150f24a6fffb3ce5be32705e6a27160df055b3582925c2ed170f4f430 \ - --hash=sha256:a05f899c311f6810ae4981edcd23d1587be207de554cf0530f8facbe836483cb \ - --hash=sha256:91970de4b3dbf0b9b36745e9f346265d225d906188dec3d02c2179fbdb49b167 \ - --hash=sha256:908cd59ae3c177c28e7a3eb519dade45748ba9af9959796c699f4f1b56caea8d \ - --hash=sha256:f04fd7c4b7b4c0b97186f31a315fe88d20087a7148ff06a9c0348b35e39531f8 \ - --hash=sha256:757dd412a49ea396b52b051c364bf8f9262dfa6665270f68208a0d6ed5999f1b \ - --hash=sha256:7d6ab4507cf52328b27c57107491b2699a5e25097213a2d201fab0157cb5dd09 \ - --hash=sha256:e3183550d129b7103914cad049a3b2358d9405c6e4baf3a7c92a82004f5e4814 \ - --hash=sha256:9d1f63e2f4530a919ef87b4f1b3d814389604486f8b8c090aefccace4d1f98f8 \ - --hash=sha256:8e88ebac371a388024dab3ccf393bf3c1790d21bc3c299d5a6f9f83fb823beda +cryptography==2.0.2 \ + --hash=sha256:187ae17358436d2c760f28c2aeb02fefa3f37647a9c5b6f7f7c3e83cd1c5a972 \ + --hash=sha256:19e43a13bbf52028dd1e810c803f2ad8880d0692d772f98d42e1eaf34bdee3d6 \ + --hash=sha256:da9291502cbc87dc0284a20c56876e4d2e68deac61cc43df4aec934e44ca97b1 \ + --hash=sha256:0954f8813095f581669330e0a2d5e726c33ac7f450c1458fac58bab54595e516 \ + --hash=sha256:d68b0cc40a8432ed3fc84876c519de704d6001800ec22b136e75ae841910c45b \ + --hash=sha256:2f8ad9580ab4da645cfea52a91d2da99a49a1e76616d8be68441a986fad652b0 \ + --hash=sha256:cc00b4511294f5f6b65c4e77a1a9c62f52490a63d2c120f3872176b40a82351e \ + --hash=sha256:cf896020f6a9f095a547b3d672c8db1ef2ed71fca11250731fa1d4a4cb8b1590 \ + --hash=sha256:e0fdb8322206fa02aa38f71519ff75dce2eb481b7e1110e2936795cb376bb6ee \ + --hash=sha256:277538466657ca5d6637f80be100242f9831d75138b788d718edd3aab34621f8 \ + --hash=sha256:2c77eb0560f54ce654ab82d6b2a64327a71ee969b29022bf9746ca311c9f5069 \ + --hash=sha256:755a7853b679e79d0a799351c092a9b0271f95ff54c8dd8823d8b527a2926a86 \ + --hash=sha256:77197a2d525e761cdd4c771180b4bd0d80703654c6385e4311cbbbe2beb56fa1 \ + --hash=sha256:eb8bb79d0ab00c931c8333b745f06fec481a51c52d70acd4ee95d6093ba5c386 \ + --hash=sha256:131f61de82ef28f3e20beb4bfc24f9692d28cecfd704e20e6c7f070f7793013a \ + --hash=sha256:ac35435974b2e27cd4520f29c191d7da36f4189aa3264e52c4c6c6d089ab6142 \ + --hash=sha256:04b6ea99daa2a8460728794213d76d45ad58ea247dc7e7ff148d7dd726e87863 \ + --hash=sha256:2b9442f8b4c3d575f6cc3db0e856034e0f5a9d55ecd636f52d8c496795b26952 \ + --hash=sha256:b3d3b3ecba1fe1bdb6f180770a137f877c8f07571f7b2934bb269475bcf0e5e8 \ + --hash=sha256:670a58c0d75cb0e78e73dd003bd96d4440bbb1f2bc041dcf7b81767ca4fb0ce9 \ + --hash=sha256:5af84d23bdb86b5e90aca263df1424b43f1748480bfcde3ac2a3cbe622612468 \ + --hash=sha256:ba22e8eefabdd7aca37d0c0c00d2274000d2cebb5cce9e5a710cb55bf8797b31 \ + --hash=sha256:b798b22fa7e92b439547323b8b719d217f1e1b7677585cfeeedf3b55c70bb7fb \ + --hash=sha256:59cff28af8cce96cb7e94a459726e1d88f6f5fa75097f9dcbebd99118d64ea4c \ + --hash=sha256:fe859e445abc9ba9e97950ddafb904e23234c4ecb76b0fae6c86e80592ce464a \ + --hash=sha256:655f3c474067f1e277430f23cc0549f0b1dc99b82aec6e53f80b9b2db7f76f11 \ + --hash=sha256:0ebc2be053c9a03a2f3e20a466e87bf12a51586b3c79bd2a22171b073a805346 \ + --hash=sha256:01e6e60654df64cca53733cda39446d67100c819c181d403afb120e0d2a71e1b \ + --hash=sha256:d46f4e5d455cb5563685c52ef212696f0a6cc1ea627603218eabbd8a095291d8 \ + --hash=sha256:3780b2663ee7ebb37cb83263326e3cd7f8b2ea439c448539d4b87de12c8d06ab enum34==1.1.2 \ --hash=sha256:2475d7fcddf5951e92ff546972758802de5260bf409319a9f1934e6bbc8b1dc7 \ --hash=sha256:35907defb0f992b75ab7788f65fedc1cf20ffa22688e0e6f6f12afc06b3ea501 @@ -864,18 +907,18 @@ letsencrypt==0.7.0 \ --hash=sha256:105a5fb107e45bcd0722eb89696986dcf5f08a86a321d6aef25a0c7c63375ade \ --hash=sha256:c36e532c486a7e92155ee09da54b436a3c420813ec1c590b98f635d924720de9 -certbot==0.15.0 \ - --hash=sha256:f052a1ee9d0e71b73d893c26ee3aa343f6f3abe7de8471437779d541f8bf7824 \ - --hash=sha256:b8c4043b2b8df39660d4ce4a2a6eca590f98ece0e1b97eba53ab95f3bbac3beb -acme==0.15.0 \ - --hash=sha256:d423a14a8fde089d6854ccbe1314f6a80553ef06799ac6f671b90d8399835e60 \ - --hash=sha256:9fadd63322a1eb95f58e6cda8ca2095c750e828ae470bc6e3925ef618c7cfc87 -certbot-apache==0.15.0 \ - --hash=sha256:07fa99b264e0ea489695cc2a5353f3fe9459422ad549de02c46da24ae546acd9 \ - --hash=sha256:6da1433381bd2c2ea7c395be57ca1bcdb7c1c04ce3d12b67a3fa207a3bfa41ca -certbot-nginx==0.15.0 \ - --hash=sha256:7b0622f8a9031e24105f9b5c8d98d4ed83ae393517ed35cc2a4fa50098122922 \ - --hash=sha256:0b98dedb22492d6f88dffdfbd721b4d4c98bfe361df35bc97bf5bd3047f01234 +certbot==0.17.0 \ + --hash=sha256:64c25c7123357feffded6408660bc6f5c7d493dd635ae172081d21473075a86a \ + --hash=sha256:43f5b26c3f314d14babf79a3bdf3522e4fc9eef867a0681c426f113c650a669c +acme==0.17.0 \ + --hash=sha256:501710171633af13fc52aa61d0277a6fe335f7477db5810e72239aaf4f3a09e7 \ + --hash=sha256:3ccbe4aaeb98c77b98ee4093b4e4adb76a1a24cbdfec0130c489c206f1d9b66e +certbot-apache==0.17.0 \ + --hash=sha256:17a7e8d7526d838610e68b96cf052af17c4055655b76b06d1cbc74857d90a216 \ + --hash=sha256:29b9e7bc5eaaff6dc4bce8398e35eeacdf346126aad68cac3d41bb87df20a6b9 +certbot-nginx==0.17.0 \ + --hash=sha256:980c9a33a79ab839a089a0085ff0c5414f01f47b6db26ed342df25916658cec9 \ + --hash=sha256:e573f8b4283172755c07b9cca8a8da7ef2d31b4df763881394b5339b2d42994a UNLIKELY_EOF # ------------------------------------------------------------------------- @@ -903,6 +946,7 @@ anything goes wrong, it will exit with a non-zero status code. # 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 distutils.version import StrictVersion from hashlib import sha256 from os.path import join from pipes import quote @@ -937,12 +981,14 @@ except ImportError: from urllib.parse import urlparse # 3.4 -__version__ = 1, 1, 1 +__version__ = 1, 3, 0 +PIP_VERSION = '9.0.1' # wheel has a conditional dependency on argparse: maybe_argparse = ( - [('https://pypi.python.org/packages/source/a/argparse/' + [('https://pypi.python.org/packages/18/dd/' + 'e617cfc3f6210ae183374cd9f6a26b20514bbb5a792af97949c5aacddf0f/' 'argparse-1.4.0.tar.gz', '62b089a55be1d8949cd2bc7e0df0bddb9e028faefc8c32038cc84862aefdd6e4')] if version_info < (2, 7, 0) else []) @@ -950,13 +996,19 @@ maybe_argparse = ( 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'), + ('https://pypi.python.org/packages/11/b6/' + 'abcb525026a4be042b486df43905d6893fb04f05aac21c32c638e939e447/' + 'pip-{0}.tar.gz' + .format(PIP_VERSION), + '09f243e1a7b461f654c26a725fa373211bb7ff17a9300058b205c61658ca940d'), # This version of setuptools has only optional dependencies: - ('https://pypi.python.org/packages/source/s/setuptools/' + ('https://pypi.python.org/packages/69/65/' + '4c544cde88d4d876cdf5cbc5f3f15d02646477756d89547e9a7ecd6afa76/' 'setuptools-20.2.2.tar.gz', '24fcfc15364a9fe09a220f37d2dcedc849795e3de3e4b393ee988e66a9cbd85a'), - ('https://pypi.python.org/packages/source/w/wheel/wheel-0.29.0.tar.gz', + ('https://pypi.python.org/packages/c9/1d/' + 'bd19e691fd4cfe908c76c429fe6e4436c9e83583c4414b54f6c85471954a/' + 'wheel-0.29.0.tar.gz', '1ebb8ad7e26b448e9caa4773d2357849bf80ff9e313964bcaf79cbf0201a1648') ] @@ -1006,11 +1058,21 @@ def hashed_download(url, temp, digest): def main(): + pip_version = StrictVersion(check_output(['pip', '--version']) + .decode('utf-8').split()[1]) + min_pip_version = StrictVersion(PIP_VERSION) + if pip_version >= min_pip_version: + return 0 + has_pip_cache = pip_version >= StrictVersion('6.0') + 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 ' + + # Disable cache since we're not using it and it otherwise + # sometimes throws permission warnings: + ('--no-cache-dir ' if has_pip_cache else '') + ' '.join(quote(d) for d in downloads), shell=True) except HashError as exc: @@ -1033,9 +1095,9 @@ UNLIKELY_EOF PATH="$VENV_BIN:$PATH" "$VENV_BIN/python" "$TEMP_DIR/pipstrap.py" set +e if [ "$VERBOSE" = 1 ]; then - "$VENV_BIN/pip" install --no-cache-dir --require-hashes -r "$TEMP_DIR/letsencrypt-auto-requirements.txt" + "$VENV_BIN/pip" install --disable-pip-version-check --no-cache-dir --require-hashes -r "$TEMP_DIR/letsencrypt-auto-requirements.txt" else - PIP_OUT=`"$VENV_BIN/pip" install --no-cache-dir --require-hashes -r "$TEMP_DIR/letsencrypt-auto-requirements.txt" 2>&1` + PIP_OUT=`"$VENV_BIN/pip" install --disable-pip-version-check --no-cache-dir --require-hashes -r "$TEMP_DIR/letsencrypt-auto-requirements.txt" 2>&1` fi PIP_STATUS=$? set -e diff --git a/certbot-compatibility-test/Dockerfile b/certbot-compatibility-test/Dockerfile index bb9359ce8..fe55a68a6 100644 --- a/certbot-compatibility-test/Dockerfile +++ b/certbot-compatibility-test/Dockerfile @@ -8,8 +8,8 @@ MAINTAINER Brad Warren <bmw@eff.org> # TODO: Install non-default Python versions for tox. # TODO: Install Apache/Nginx for plugin development. -COPY certbot-auto /opt/certbot/src/certbot-auto -RUN /opt/certbot/src/certbot-auto -n --os-packages-only +COPY letsencrypt-auto-source /opt/certbot/src/letsencrypt-auto-source +RUN /opt/certbot/src/letsencrypt-auto-source/letsencrypt-auto --os-packages-only # the above is not likely to change, so by putting it further up the # Dockerfile we make sure we cache as much as possible @@ -29,16 +29,18 @@ COPY acme /opt/certbot/src/acme/ COPY certbot-apache /opt/certbot/src/certbot-apache/ COPY certbot-nginx /opt/certbot/src/certbot-nginx/ COPY certbot-compatibility-test /opt/certbot/src/certbot-compatibility-test/ +COPY tools /opt/certbot/src/tools RUN virtualenv --no-site-packages -p python2 /opt/certbot/venv && \ /opt/certbot/venv/bin/pip install -U setuptools && \ - /opt/certbot/venv/bin/pip install -U pip && \ - /opt/certbot/venv/bin/pip install \ - -e /opt/certbot/src/acme \ - -e /opt/certbot/src \ - -e /opt/certbot/src/certbot-apache \ - -e /opt/certbot/src/certbot-nginx \ - -e /opt/certbot/src/certbot-compatibility-test + /opt/certbot/venv/bin/pip install -U pip +ENV PATH /opt/certbot/venv/bin:$PATH +RUN /opt/certbot/src/tools/pip_install_editable.sh \ + /opt/certbot/src/acme \ + /opt/certbot/src \ + /opt/certbot/src/certbot-apache \ + /opt/certbot/src/certbot-nginx \ + /opt/certbot/src/certbot-compatibility-test # install in editable mode (-e) to save space: it's not possible to # "rm -rf /opt/certbot/src" (it's stays in the underlaying image); @@ -46,5 +48,3 @@ RUN virtualenv --no-site-packages -p python2 /opt/certbot/venv && \ # bash" and investigate, apply patches, etc. WORKDIR /opt/certbot/src/certbot-compatibility-test/certbot_compatibility_test/testdata - -ENV PATH /opt/certbot/venv/bin:$PATH diff --git a/certbot-compatibility-test/certbot_compatibility_test/configurators/nginx/common.py b/certbot-compatibility-test/certbot_compatibility_test/configurators/nginx/common.py index 3622bee41..ed5cf750e 100644 --- a/certbot-compatibility-test/certbot_compatibility_test/configurators/nginx/common.py +++ b/certbot-compatibility-test/certbot_compatibility_test/configurators/nginx/common.py @@ -79,6 +79,8 @@ def _get_names(config): if line.strip().startswith("server_name"): names = line.partition("server_name")[2].rpartition(";")[0] for n in names.split(): - all_names.add(n) + # Filter out wildcards in both all_names and test_names + if not n.startswith("*."): + all_names.add(n) non_ip_names = set(n for n in all_names if not util.IP_REGEX.match(n)) return all_names, non_ip_names diff --git a/certbot-compatibility-test/certbot_compatibility_test/test_driver.py b/certbot-compatibility-test/certbot_compatibility_test/test_driver.py index 71100bb27..71a0ba574 100644 --- a/certbot-compatibility-test/certbot_compatibility_test/test_driver.py +++ b/certbot-compatibility-test/certbot_compatibility_test/test_driver.py @@ -1,7 +1,6 @@ """Tests Certbot plugins against different server configurations.""" import argparse import filecmp -import functools import logging import os import shutil @@ -64,17 +63,17 @@ 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].chall, - achalls[i].domain, - util.JWK.public_key(), - host="127.0.0.1", - port=plugin.https_port) - if _try_until_true(verify): + verified = responses[i].simple_verify(achalls[i].chall, + achalls[i].domain, + util.JWK.public_key(), + host="127.0.0.1", + port=plugin.https_port) + if verified: logger.info( "tls-sni-01 verification for %s succeeded", achalls[i].domain) else: logger.error( - "tls-sni-01 verification for %s in %s failed", + "**** tls-sni-01 verification for %s in %s failed", achalls[i].domain, config) success = False @@ -122,7 +121,7 @@ def test_installer(args, plugin, config, temp_dir): if names_match: logger.info("get_all_names test succeeded") else: - logger.error("get_all_names test failed for config %s", config) + logger.error("**** get_all_names test failed for config %s", config) domains = list(plugin.get_testable_domain_names()) success = test_deploy_cert(plugin, temp_dir, domains) @@ -147,7 +146,7 @@ def test_deploy_cert(plugin, temp_dir, domains): plugin.deploy_cert(domain, cert_path, util.KEY_PATH, cert_path, cert_path) plugin.save() # Needed by the Apache plugin except le_errors.Error as error: - logger.error("Plugin failed to deploy certificate for %s:", domain) + logger.error("**** Plugin failed to deploy certificate for %s:", domain) logger.exception(error) return False @@ -155,11 +154,12 @@ def test_deploy_cert(plugin, temp_dir, domains): return False success = True + time.sleep(3) for domain in domains: - verify = functools.partial(validator.Validator().certificate, cert, - domain, "127.0.0.1", plugin.https_port) - if not _try_until_true(verify): - logger.error("Could not verify certificate for domain %s", domain) + verified = validator.Validator().certificate( + cert, domain, "127.0.0.1", plugin.https_port) + if not verified: + logger.error("**** Could not verify certificate for domain %s", domain) success = False if success: @@ -177,16 +177,21 @@ def test_enhancements(plugin, domains): "enhancements") return False - for domain in domains: + domains_and_info = [(domain, []) for domain in domains] + + for domain, info in domains_and_info: try: + previous_redirect = validator.Validator().any_redirect( + "localhost", plugin.http_port, headers={"Host": domain}) + info.append(previous_redirect) 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) + logger.warning("*** Plugin failed to enable redirect for %s:", domain) logger.warning("%s", error) except le_errors.Error as error: - logger.error("An error occurred while enabling redirect for %s:", + logger.error("*** An error occurred while enabling redirect for %s:", domain) logger.exception(error) @@ -194,12 +199,14 @@ def test_enhancements(plugin, domains): return False success = True - for domain in domains: - verify = functools.partial(validator.Validator().redirect, "localhost", - plugin.http_port, headers={"Host": domain}) - if not _try_until_true(verify): - logger.error("Improper redirect for domain %s", domain) - success = False + for domain, info in domains_and_info: + previous_redirect = info[0] + if not previous_redirect: + verified = validator.Validator().redirect( + "localhost", plugin.http_port, headers={"Host": domain}) + if not verified: + logger.error("*** Improper redirect for domain %s", domain) + success = False if success: logger.info("Enhancements test succeeded") @@ -207,17 +214,6 @@ def test_enhancements(plugin, domains): return success -def _try_until_true(func, max_tries=5, sleep_time=0.5): - """Calls func up to max_tries times until it returns True""" - for _ in xrange(0, max_tries): - if func(): - return True - else: - time.sleep(sleep_time) - - return False - - def _save_and_restart(plugin, title=None): """Saves and restart the plugin, returning True if no errors occurred""" try: @@ -225,7 +221,7 @@ def _save_and_restart(plugin, title=None): plugin.restart() return True except le_errors.Error as error: - logger.error("Plugin failed to save and restart server:") + logger.error("*** Plugin failed to save and restart server:") logger.exception(error) return False @@ -235,12 +231,12 @@ def test_rollback(plugin, config, backup): try: plugin.rollback_checkpoints(1337) except le_errors.Error as error: - logger.error("Plugin raised an exception during rollback:") + logger.error("*** Plugin raised an exception during rollback:") logger.exception(error) return False if _dirs_are_unequal(config, backup): - logger.error("Rollback failed for config `%s`", config) + logger.error("*** Rollback failed for config `%s`", config) return False else: logger.info("Rollback succeeded") diff --git a/certbot-compatibility-test/certbot_compatibility_test/testdata/nginx.tar.gz b/certbot-compatibility-test/certbot_compatibility_test/testdata/nginx.tar.gz Binary files differindex 4ca5cc977..2f06add17 100644 --- a/certbot-compatibility-test/certbot_compatibility_test/testdata/nginx.tar.gz +++ b/certbot-compatibility-test/certbot_compatibility_test/testdata/nginx.tar.gz diff --git a/certbot-compatibility-test/certbot_compatibility_test/validator.py b/certbot-compatibility-test/certbot_compatibility_test/validator.py index 62dd466a1..0fd6efab5 100644 --- a/certbot-compatibility-test/certbot_compatibility_test/validator.py +++ b/certbot-compatibility-test/certbot_compatibility_test/validator.py @@ -45,19 +45,12 @@ class Validator(object): else: response = requests.get(url, allow_redirects=False) - if response.status_code not in (301, 303): - return False - redirect_location = response.headers.get("location", "") # We're checking that the redirect we added behaves correctly. # It's okay for some server configuration to redirect to an # http URL, as long as it's on some other domain. if not redirect_location.startswith("https://"): - if not redirect_location.startswith("http://"): - return False - else: - if redirect_location[len("http://"):] == name: - return False + return False if response.status_code != 301: logger.error("Server did not redirect with permanent code") @@ -65,6 +58,16 @@ class Validator(object): return True + def any_redirect(self, name, port=80, headers=None): + """Test whether webserver redirects.""" + url = "http://{0}:{1}".format(name, port) + if headers: + response = requests.get(url, headers=headers, allow_redirects=False) + else: + response = requests.get(url, allow_redirects=False) + + return response.status_code in xrange(300, 309) + def hsts(self, name): """Test for HTTP Strict Transport Security header""" headers = requests.get("https://" + name).headers diff --git a/certbot-compatibility-test/setup.py b/certbot-compatibility-test/setup.py index 2b2796704..9a348f1f9 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.16.0.dev0' +version = '0.18.0.dev0' install_requires = [ 'certbot', diff --git a/certbot-dns-cloudflare/setup.py b/certbot-dns-cloudflare/setup.py index 58444b5c1..e301ae06f 100644 --- a/certbot-dns-cloudflare/setup.py +++ b/certbot-dns-cloudflare/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.16.0.dev0' +version = '0.18.0.dev0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot-dns-cloudxns/setup.py b/certbot-dns-cloudxns/setup.py index 41282f35c..61e741600 100644 --- a/certbot-dns-cloudxns/setup.py +++ b/certbot-dns-cloudxns/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.16.0.dev0' +version = '0.18.0.dev0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot-dns-digitalocean/setup.py b/certbot-dns-digitalocean/setup.py index 6b689f660..113936945 100644 --- a/certbot-dns-digitalocean/setup.py +++ b/certbot-dns-digitalocean/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.16.0.dev0' +version = '0.18.0.dev0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot-dns-dnsimple/setup.py b/certbot-dns-dnsimple/setup.py index 14f27c960..24d880bef 100644 --- a/certbot-dns-dnsimple/setup.py +++ b/certbot-dns-dnsimple/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.16.0.dev0' +version = '0.18.0.dev0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot-dns-dnsmadeeasy/setup.py b/certbot-dns-dnsmadeeasy/setup.py index f3b9b8151..cbae5303d 100644 --- a/certbot-dns-dnsmadeeasy/setup.py +++ b/certbot-dns-dnsmadeeasy/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.16.0.dev0' +version = '0.18.0.dev0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot-dns-google/setup.py b/certbot-dns-google/setup.py index ee20fd1fd..52ad15225 100644 --- a/certbot-dns-google/setup.py +++ b/certbot-dns-google/setup.py @@ -4,15 +4,17 @@ from setuptools import setup from setuptools import find_packages -version = '0.16.0.dev0' +version = '0.18.0.dev0' # Please update tox.ini when modifying dependency version requirements install_requires = [ 'acme=={0}'.format(version), 'certbot=={0}'.format(version), - 'google-api-python-client', + # 1.5 is the first version that supports oauth2client>=2.0 + 'google-api-python-client>=1.5', 'mock', - 'oauth2client', + # for oauth2client.service_account.ServiceAccountCredentials + 'oauth2client>=2.0', # For pkg_resources. >=1.0 so pip resolves it to a version cryptography # will tolerate; see #2599: 'setuptools>=1.0', diff --git a/certbot-dns-luadns/setup.py b/certbot-dns-luadns/setup.py index c964b01da..68061ced0 100644 --- a/certbot-dns-luadns/setup.py +++ b/certbot-dns-luadns/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.16.0.dev0' +version = '0.18.0.dev0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot-dns-nsone/setup.py b/certbot-dns-nsone/setup.py index 5c7bb2b5a..0a562afec 100644 --- a/certbot-dns-nsone/setup.py +++ b/certbot-dns-nsone/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.16.0.dev0' +version = '0.18.0.dev0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot-dns-rfc2136/setup.py b/certbot-dns-rfc2136/setup.py index e1d54e4bd..f225e6a89 100644 --- a/certbot-dns-rfc2136/setup.py +++ b/certbot-dns-rfc2136/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.16.0.dev0' +version = '0.18.0.dev0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot-dns-route53/setup.py b/certbot-dns-route53/setup.py index 5f6f61e4e..46fff80c6 100644 --- a/certbot-dns-route53/setup.py +++ b/certbot-dns-route53/setup.py @@ -3,7 +3,7 @@ import sys from distutils.core import setup from setuptools import find_packages -version = '0.16.0.dev0' +version = '0.18.0.dev0' install_requires = [ 'acme=={0}'.format(version), diff --git a/certbot-nginx/certbot_nginx/configurator.py b/certbot-nginx/certbot_nginx/configurator.py index 63f659453..fbe881bb0 100644 --- a/certbot-nginx/certbot_nginx/configurator.py +++ b/certbot-nginx/certbot_nginx/configurator.py @@ -87,8 +87,6 @@ class NginxConfigurator(common.Plugin): description = "Nginx Web Server plugin - Alpha" - hidden = True - DEFAULT_LISTEN_PORT = '80' @classmethod @@ -198,16 +196,10 @@ class NginxConfigurator(common.Plugin): cert_directives = [['\n', 'ssl_certificate', ' ', fullchain_path], ['\n', 'ssl_certificate_key', ' ', key_path]] - try: - self.parser.add_server_directives(vhost, - cert_directives, replace=True) - logger.info("Deployed Certificate to VirtualHost %s for %s", - vhost.filep, vhost.names) - except errors.MisconfigurationError as error: - logger.debug(error) - # Presumably break here so that the virtualhost is not modified - raise errors.PluginError("Cannot find a cert or key directive in {0} for {1}. " - "VirtualHost was not modified.".format(vhost.filep, vhost.names)) + self.parser.add_server_directives(vhost, + cert_directives, replace=True) + logger.info("Deployed Certificate to VirtualHost %s for %s", + vhost.filep, vhost.names) self.save_notes += ("Changed vhost at %s with addresses of %s\n" % (vhost.filep, diff --git a/certbot-nginx/certbot_nginx/parser.py b/certbot-nginx/certbot_nginx/parser.py index 4e4aa36ca..158cb9929 100644 --- a/certbot-nginx/certbot_nginx/parser.py +++ b/certbot-nginx/certbot_nginx/parser.py @@ -278,8 +278,8 @@ class NginxParser(object): This method modifies vhost to be fully consistent with the new directives. - ..note :: If replace is True, this raises a misconfiguration error - if the directive does not already exist. + ..note :: If replace is True and the directive already exists, the first + instance will be replaced. Otherwise, the directive is added. ..note :: If replace is False nothing gets added if an identical block exists already. @@ -464,8 +464,9 @@ def _add_directives(block, directives, replace): When replace=False, it's an error to try and add a directive that already exists in the config block with a conflicting value. - When replace=True, a directive with the same name MUST already exist in the - config block, and the first instance will be replaced. + When replace=True and a directive with the same name already exists in the + config block, the first instance will be replaced. Otherwise, the directive + will be added to the config block. ..todo :: Find directives that are in included files. @@ -547,49 +548,46 @@ def _add_directive(block, directive, replace): location = find_location(directive) if replace: - if location is None: - raise errors.MisconfigurationError( - 'expected directive for {0} in the Nginx ' - 'config but did not find it.'.format(directive[0])) - block[location] = directive - _comment_directive(block, location) - else: - # Append directive. Fail if the name is not a repeatable directive name, - # and there is already a copy of that directive with a different value - # in the config file. - - # handle flat include files - - directive_name = directive[0] - def can_append(loc, dir_name): - """ Can we append this directive to the block? """ - return loc is None or (isinstance(dir_name, str) and dir_name in REPEATABLE_DIRECTIVES) - - err_fmt = 'tried to insert directive "{0}" but found conflicting "{1}".' - - # Give a better error message about the specific directive than Nginx's "fail to restart" - if directive_name == INCLUDE: - # in theory, we might want to do this recursively, but in practice, that's really not - # necessary because we know what file we're talking about (and if we don't recurse, we - # just give a worse error message) - included_directives = _parse_ssl_options(directive[1]) - - for included_directive in included_directives: - included_dir_loc = find_location(included_directive) - included_dir_name = included_directive[0] - if not is_whitespace_or_comment(included_directive) \ - and not can_append(included_dir_loc, included_dir_name): - if block[included_dir_loc] != included_directive: - raise errors.MisconfigurationError(err_fmt.format(included_directive, - block[included_dir_loc])) - else: - _comment_out_directive(block, included_dir_loc, directive[1]) - - if can_append(location, directive_name): - block.append(directive) - _comment_directive(block, len(block) - 1) - elif block[location] != directive: - raise errors.MisconfigurationError(err_fmt.format(directive, block[location])) + if location is not None: + block[location] = directive + _comment_directive(block, location) + return + # Append directive. Fail if the name is not a repeatable directive name, + # and there is already a copy of that directive with a different value + # in the config file. + + # handle flat include files + + directive_name = directive[0] + def can_append(loc, dir_name): + """ Can we append this directive to the block? """ + return loc is None or (isinstance(dir_name, str) and dir_name in REPEATABLE_DIRECTIVES) + + err_fmt = 'tried to insert directive "{0}" but found conflicting "{1}".' + + # Give a better error message about the specific directive than Nginx's "fail to restart" + if directive_name == INCLUDE: + # in theory, we might want to do this recursively, but in practice, that's really not + # necessary because we know what file we're talking about (and if we don't recurse, we + # just give a worse error message) + included_directives = _parse_ssl_options(directive[1]) + + for included_directive in included_directives: + included_dir_loc = find_location(included_directive) + included_dir_name = included_directive[0] + if not is_whitespace_or_comment(included_directive) \ + and not can_append(included_dir_loc, included_dir_name): + if block[included_dir_loc] != included_directive: + raise errors.MisconfigurationError(err_fmt.format(included_directive, + block[included_dir_loc])) + else: + _comment_out_directive(block, included_dir_loc, directive[1]) + + if can_append(location, directive_name): + block.append(directive) + _comment_directive(block, len(block) - 1) + elif block[location] != directive: + raise errors.MisconfigurationError(err_fmt.format(directive, block[location])) def _apply_global_addr_ssl(addr_to_ssl, parsed_server): """Apply global sslishness information to the parsed server block diff --git a/certbot-nginx/certbot_nginx/tests/parser_test.py b/certbot-nginx/certbot_nginx/tests/parser_test.py index 3877bf5d4..e655bc3e3 100644 --- a/certbot-nginx/certbot_nginx/tests/parser_test.py +++ b/certbot-nginx/certbot_nginx/tests/parser_test.py @@ -273,11 +273,16 @@ class NginxParserTest(util.NginxTest): #pylint: disable=too-many-public-methods ['server_name', 'example.*'], [] ]]]) mock_vhost.names = set(['foobar.com', 'example.*']) - self.assertRaises(errors.MisconfigurationError, - nparser.add_server_directives, - mock_vhost, - [['ssl_certificate', 'cert.pem']], - replace=True) + nparser.add_server_directives( + mock_vhost, [['ssl_certificate', 'cert.pem']], replace=True) + self.assertEqual( + nparser.parsed[filep], + [[['server'], [['listen', '69.50.225.155:9000'], + ['listen', '127.0.0.1'], + ['server_name', 'foobar.com'], ['#', COMMENT], + ['server_name', 'example.*'], [], + ['ssl_certificate', 'cert.pem'], ['#', COMMENT], [], + ]]]) def test_get_best_match(self): target_name = 'www.eff.org' diff --git a/certbot-nginx/setup.py b/certbot-nginx/setup.py index 699c6bba6..f89307816 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.16.0.dev0' +version = '0.18.0.dev0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot/__init__.py b/certbot/__init__.py index b372841e0..6c5eff32b 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.16.0.dev0' +__version__ = '0.18.0.dev0' diff --git a/certbot/achallenges.py b/certbot/achallenges.py index c2af45fdb..f39bb4cec 100644 --- a/certbot/achallenges.py +++ b/certbot/achallenges.py @@ -28,7 +28,6 @@ logger = logging.getLogger(__name__) # pylint: disable=too-few-public-methods - class AnnotatedChallenge(jose.ImmutableMap): """Client annotated challenge. diff --git a/certbot/cert_manager.py b/certbot/cert_manager.py index 62cbfa695..207e4d072 100644 --- a/certbot/cert_manager.py +++ b/certbot/cert_manager.py @@ -205,7 +205,7 @@ def _report_human_readable(config, parsed_certs): " Certificate Path: {3}\n" " Private Key Path: {4}".format( cert.lineagename, - " ".join(cert.names()), + ",".join(cert.names()), valid_string, cert.fullchain, cert.privkey)) diff --git a/certbot/cli.py b/certbot/cli.py index 4c5505506..bb29a43ec 100644 --- a/certbot/cli.py +++ b/certbot/cli.py @@ -440,8 +440,8 @@ class HelpfulArgumentParser(object): } # List of topics for which additional help can be provided - HELP_TOPICS = ["all", "security", "paths", "automation", "testing"] + list(self.VERBS) - HELP_TOPICS += self.COMMANDS_TOPICS + ["manage"] + HELP_TOPICS = ["all", "security", "paths", "automation", "testing"] + HELP_TOPICS += list(self.VERBS) + self.COMMANDS_TOPICS + ["manage"] plugin_names = list(plugins) self.help_topics = HELP_TOPICS + plugin_names + [None] @@ -850,6 +850,12 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis None, "-t", "--text", dest="text_mode", action="store_true", help=argparse.SUPPRESS) helpful.add( + None, "--max-log-backups", type=nonnegative_int, default=1000, + help="Specifies the maximum number of backup logs that should " + "be kept by Certbot's built in log rotation. Setting this " + "flag to 0 disables log rotation entirely, causing " + "Certbot to always append to the same log file.") + helpful.add( [None, "automation", "run", "certonly"], "-n", "--non-interactive", "--noninteractive", dest="noninteractive_mode", action="store_true", help="Run without ever asking for user input. This may require " @@ -894,7 +900,7 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis " in order to obtain test certificates, 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.") + " renewal. --deploy-hook commands are not called.") helpful.add( ["register", "automation"], "--register-unsafely-without-email", action="store_true", help="Specifying this flag enables registering an account with no " @@ -1092,24 +1098,28 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis " run if an attempt was made to obtain/renew a certificate. If" " multiple renewed certificates have identical post-hooks, only" " one will be run.") + helpful.add("renew", "--renew-hook", + action=_RenewHookAction, help=argparse.SUPPRESS) helpful.add( - "renew", "--renew-hook", - help="Command to be run in a shell once for each successfully renewed" - " certificate. For this command, the shell variable $RENEWED_LINEAGE" - " will point to the config live subdirectory (for example," - " \"/etc/letsencrypt/live/example.com\") containing the new certificates" - " and keys; the shell variable $RENEWED_DOMAINS will contain a" - " space-delimited list of renewed certificate domains (for example," - " \"example.com www.example.com\"") + "renew", "--deploy-hook", action=_DeployHookAction, + help="Command to be run in a shell once for each successfully" + " issued certificate. For this command, the shell variable" + " $RENEWED_LINEAGE will point to the config live subdirectory" + ' (for example, "/etc/letsencrypt/live/example.com") containing' + " the new certificates and keys; the shell variable" + " $RENEWED_DOMAINS will contain a space-delimited list of" + ' renewed certificate domains (for example, "example.com' + ' www.example.com"') 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." + " --pre-hook/--post-hook/--deploy-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: False)") helpful.add_deprecated_argument("--agree-dev-preview", 0) @@ -1142,15 +1152,21 @@ def _create_subparsers(helpful): '(default: {0}). The flags encoded in the user agent are: ' '--duplicate, --force-renew, --allow-subset-of-names, -n, and ' 'whether any hooks are set.'.format(sample_user_agent())) + helpful.add( + None, "--user-agent-comment", default=None, type=_user_agent_comment_type, + help="Add a comment to the default user agent string. May be used when repackaging Certbot " + "or calling it from another tool to allow additional statistical data to be collected." + " Ignored if --user-agent is set. (Example: Foo-Wrapper/1.0)") helpful.add("certonly", "--csr", type=read_file, help="Path to a Certificate Signing Request (CSR) in DER or PEM format." " Currently --csr only works with the 'certonly' subcommand.") helpful.add("revoke", "--reason", dest="reason", - choices=CaseInsensitiveList(constants.REVOCATION_REASONS.keys()), + choices=CaseInsensitiveList(sorted(constants.REVOCATION_REASONS, + key=constants.REVOCATION_REASONS.get)), action=_EncodeReasonAction, default=0, - help="Specify reason for revoking certificate.") + help="Specify reason for revoking certificate. (default: unspecified)") helpful.add("rollback", "--checkpoints", type=int, metavar="N", default=flag_default("rollback_checkpoints"), @@ -1356,3 +1372,53 @@ def parse_preferred_challenges(pref_challs): raise errors.Error( "Unrecognized challenges: {0}".format(unrecognized)) return challs + +def _user_agent_comment_type(value): + if "(" in value or ")" in value: + raise argparse.ArgumentTypeError("may not contain parentheses") + return value + +class _DeployHookAction(argparse.Action): + """Action class for parsing deploy hooks.""" + + def __call__(self, parser, namespace, values, option_string=None): + renew_hook_set = namespace.deploy_hook != namespace.renew_hook + if renew_hook_set and namespace.renew_hook != values: + raise argparse.ArgumentError( + self, "conflicts with --renew-hook value") + namespace.deploy_hook = namespace.renew_hook = values + + +class _RenewHookAction(argparse.Action): + """Action class for parsing renew hooks.""" + + def __call__(self, parser, namespace, values, option_string=None): + deploy_hook_set = namespace.deploy_hook is not None + if deploy_hook_set and namespace.deploy_hook != values: + raise argparse.ArgumentError( + self, "conflicts with --deploy-hook value") + namespace.renew_hook = values + + +def nonnegative_int(value): + """Converts value to an int and checks that it is not negative. + + This function should used as the type parameter for argparse + arguments. + + :param str value: value provided on the command line + + :returns: integer representation of value + :rtype: int + + :raises argparse.ArgumentTypeError: if value isn't a non-negative integer + + """ + try: + int_value = int(value) + except ValueError: + raise argparse.ArgumentTypeError("value must be an integer") + + if int_value < 0: + raise argparse.ArgumentTypeError("value must be non-negative") + return int_value diff --git a/certbot/client.py b/certbot/client.py index 6010dd0a0..ed70fda71 100644 --- a/certbot/client.py +++ b/certbot/client.py @@ -58,11 +58,12 @@ def determine_user_agent(config): # policy, talk to a core Certbot team member before making any # changes here. if config.user_agent is None: - ua = ("CertbotACMEClient/{0} ({1}; {2}) Authenticator/{3} Installer/{4} " + ua = ("CertbotACMEClient/{0} ({1}; {2}{8}) Authenticator/{3} Installer/{4} " "({5}; flags: {6}) Py/{7}") ua = ua.format(certbot.__version__, cli.cli_command, util.get_os_info_ua(), config.authenticator, config.installer, config.verb, - ua_flags(config), platform.python_version()) + ua_flags(config), platform.python_version(), + "; " + config.user_agent_comment if config.user_agent_comment else "") else: ua = config.user_agent return ua diff --git a/certbot/crypto_util.py b/certbot/crypto_util.py index e22effeb7..112ef7c85 100644 --- a/certbot/crypto_util.py +++ b/certbot/crypto_util.py @@ -214,7 +214,7 @@ def verify_renewable_cert(renewable_cert): """ verify_renewable_cert_sig(renewable_cert) verify_fullchain(renewable_cert) - verify_cert_matches_priv_key(renewable_cert) + verify_cert_matches_priv_key(renewable_cert.cert, renewable_cert.privkey) def verify_renewable_cert_sig(renewable_cert): @@ -238,27 +238,24 @@ def verify_renewable_cert_sig(renewable_cert): raise errors.Error(error_str) -def verify_cert_matches_priv_key(renewable_cert): +def verify_cert_matches_priv_key(cert_path, key_path): """ Verifies that the private key and cert match. - :param `.storage.RenewableCert` renewable_cert: cert to verify + :param str cert_path: path to a cert in PEM format + :param str key_path: path to a private key file :raises errors.Error: If they don't match. """ try: - with open(renewable_cert.cert) as cert: - cert = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, cert.read()) - with open(renewable_cert.privkey) as privkey: - privkey = OpenSSL.crypto.load_privatekey(OpenSSL.crypto.FILETYPE_PEM, privkey.read()) context = OpenSSL.SSL.Context(OpenSSL.SSL.SSLv23_METHOD) - context.use_privatekey(privkey) - context.use_certificate(cert) + context.use_certificate_file(cert_path) + context.use_privatekey_file(key_path) context.check_privatekey() except (IOError, OpenSSL.SSL.Error) as e: error_str = "verifying the cert located at {0} matches the \ private key located at {1} has failed. \ - Details: {2}".format(renewable_cert.cert, - renewable_cert.privkey, e) + Details: {2}".format(cert_path, + key_path, e) logger.exception(error_str) raise errors.Error(error_str) diff --git a/certbot/display/enhancements.py b/certbot/display/enhancements.py index d2ffe2e0d..0f6b6c57d 100644 --- a/certbot/display/enhancements.py +++ b/certbot/display/enhancements.py @@ -42,12 +42,14 @@ def redirect_by_default(): """ choices = [ - ("Easy", "Allow both HTTP and HTTPS access to these sites"), - ("Secure", "Make all requests redirect to secure HTTPS access"), + ("No redirect", "Make no further changes to the webserver configuration."), + ("Redirect", "Make all requests redirect to secure HTTPS access. " + "Choose this for new sites, or if you're confident your site works on HTTPS. " + "You can undo this change by editing your web server's configuration."), ] code, selection = util(interfaces.IDisplay).menu( - "Please choose whether HTTPS access is required or optional.", + "Please choose whether or not to redirect HTTP traffic to HTTPS, removing HTTP access.", choices, default=0, cli_flag="--redirect / --no-redirect", force_interactive=True) diff --git a/certbot/hooks.py b/certbot/hooks.py index b3c1fc3e2..799ef90b7 100644 --- a/certbot/hooks.py +++ b/certbot/hooks.py @@ -18,6 +18,7 @@ def validate_hooks(config): """Check hook commands are executable.""" validate_hook(config.pre_hook, "pre") validate_hook(config.post_hook, "post") + validate_hook(config.deploy_hook, "deploy") validate_hook(config.renew_hook, "renew") @@ -95,16 +96,30 @@ def run_saved_post_hooks(): _run_hook(cmd) +def deploy_hook(config, domains, lineage_path): + """Run post-issuance hook if defined. + + :param configuration.NamespaceConfig config: Certbot settings + :param domains: domains in the obtained certificate + :type domains: `list` of `str` + :param str lineage_path: live directory path for the new cert + + """ + if config.deploy_hook: + renew_hook(config, domains, lineage_path) + + def renew_hook(config, domains, lineage_path): """Run post-renewal hook if defined.""" if config.renew_hook: if not config.dry_run: os.environ["RENEWED_DOMAINS"] = " ".join(domains) os.environ["RENEWED_LINEAGE"] = lineage_path - logger.info("Running renew-hook command: %s", config.renew_hook) + logger.info("Running deploy-hook command: %s", config.renew_hook) _run_hook(config.renew_hook) else: - logger.warning("Dry run: skipping renewal hook command: %s", config.renew_hook) + logger.warning( + "Dry run: skipping deploy hook command: %s", config.renew_hook) def _run_hook(shell_cmd): diff --git a/certbot/log.py b/certbot/log.py index 889b5c50a..73b2e354f 100644 --- a/certbot/log.py +++ b/certbot/log.py @@ -138,7 +138,8 @@ def setup_log_file_handler(config, logfile, fmt): log_file_path = os.path.join(config.logs_dir, logfile) try: handler = logging.handlers.RotatingFileHandler( - log_file_path, maxBytes=2 ** 20, backupCount=1000) + log_file_path, maxBytes=2 ** 20, + backupCount=config.max_log_backups) except IOError as error: raise errors.Error(util.PERM_ERR_FMT.format(error)) # rotate on each invocation, rollover only possible when maxBytes diff --git a/certbot/main.py b/certbot/main.py index cd87706b4..a6e630540 100644 --- a/certbot/main.py +++ b/certbot/main.py @@ -82,6 +82,8 @@ def _get_and_save_cert(le_client, config, domains=None, certname=None, lineage=N lineage = le_client.obtain_and_enroll_certificate(domains, certname) if lineage is False: raise errors.Error("Certificate could not be obtained") + elif lineage is not None: + hooks.deploy_hook(config, lineage.names(), lineage.live_dir) finally: hooks.post_hook(config) @@ -291,11 +293,12 @@ def _find_domains_or_certname(config, installer): return domains, certname -def _report_new_cert(config, cert_path, fullchain_path): +def _report_new_cert(config, cert_path, fullchain_path, key_path=None): """Reports the creation of a new certificate to the user. :param str cert_path: path to cert :param str fullchain_path: path to full chain + :param str key_path: path to private key, if available """ if config.dry_run: @@ -310,13 +313,17 @@ def _report_new_cert(config, cert_path, fullchain_path): # (Nginx and Apache2.4) will want. verbswitch = ' with the "certonly" option' if config.verb == "run" else "" + privkey_statement = 'Your key file has been saved at:{br}{0}{br}'.format( + key_path, br=os.linesep) if key_path else "" # XXX Perhaps one day we could detect the presence of known old webservers # and say something more informative here. - msg = ('Congratulations! Your certificate and chain have been saved at {0}.' - ' Your cert will expire on {1}. To obtain a new or tweaked version of this ' - 'certificate in the future, simply run {2} again{3}. ' - 'To non-interactively renew *all* of your certificates, run "{2} renew"' - .format(fullchain_path, expiry, cli.cli_command, verbswitch)) + msg = ('Congratulations! Your certificate and chain have been saved at:{br}' + '{0}{br}{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 certificates, run "{3} renew"' + .format(fullchain_path, privkey_statement, expiry, cli.cli_command, verbswitch, + br=os.linesep)) reporter_util.add_message(msg, reporter_util.MEDIUM_PRIORITY) @@ -560,6 +567,7 @@ def revoke(config, unused_plugins): # TODO: coop with renewal config if config.key_path is not None: # revocation by cert key logger.debug("Revoking %s using cert key %s", config.cert_path[0], config.key_path[0]) + crypto_util.verify_cert_matches_priv_key(config.cert_path[0], config.key_path[0]) key = jose.JWK.load(config.key_path[1]) else: # revocation by account key logger.debug("Revoking %s using Account Key", config.cert_path[0]) @@ -599,7 +607,8 @@ def run(config, plugins): # pylint: disable=too-many-branches,too-many-locals cert_path = new_lineage.cert_path if new_lineage else None fullchain_path = new_lineage.fullchain_path if new_lineage else None - _report_new_cert(config, cert_path, fullchain_path) + key_path = new_lineage.key_path if new_lineage else None + _report_new_cert(config, cert_path, fullchain_path, key_path) _install_cert(config, le_client, domains, new_lineage) @@ -684,7 +693,8 @@ def certonly(config, plugins): cert_path = lineage.cert_path if lineage else None fullchain_path = lineage.fullchain_path if lineage else None - _report_new_cert(config, cert_path, fullchain_path) + key_path = lineage.key_path if lineage else None + _report_new_cert(config, cert_path, fullchain_path, key_path) _suggest_donation_if_appropriate(config) def renew(config, unused_plugins): diff --git a/certbot/plugins/selection.py b/certbot/plugins/selection.py index fdfbf2b15..9e2ddf609 100644 --- a/certbot/plugins/selection.py +++ b/certbot/plugins/selection.py @@ -108,11 +108,19 @@ def choose_plugin(prepared, question): opts = [plugin_ep.description_with_name + (" [Misconfigured]" if plugin_ep.misconfigured else "") for plugin_ep in prepared] + names = set(plugin_ep.name for plugin_ep in prepared) while True: disp = z_util(interfaces.IDisplay) - code, index = disp.menu( - question, opts, force_interactive=True) + if "CERTBOT_AUTO" in os.environ and names == set(("apache", "nginx")): + # The possibility of being offered exactly apache and nginx here + # is new interactivity brought by https://github.com/certbot/certbot/issues/4079, + # so set apache as a default for those kinds of non-interactive use + # (the user will get a warning to set --non-interactive or --force-interactive) + apache_idx = [n for n, p in enumerate(prepared) if p.name == "apache"][0] + code, index = disp.menu(question, opts, default=apache_idx) + else: + code, index = disp.menu(question, opts, force_interactive=True) if code == display_util.OK: plugin_ep = prepared[index] diff --git a/certbot/plugins/selection_test.py b/certbot/plugins/selection_test.py index 9f0716905..4112810a2 100644 --- a/certbot/plugins/selection_test.py +++ b/certbot/plugins/selection_test.py @@ -1,4 +1,5 @@ """Tests for letsencrypt.plugins.selection""" +import os import sys import unittest @@ -115,6 +116,7 @@ class ChoosePluginTest(unittest.TestCase): False)) self.mock_apache = mock.Mock( description_with_name="a", misconfigured=True) + self.mock_apache.name = "apache" self.mock_stand = mock.Mock( description_with_name="s", misconfigured=False) self.mock_stand.init().more_info.return_value = "standalone" @@ -146,3 +148,26 @@ class ChoosePluginTest(unittest.TestCase): def test_no_choice(self, mock_util): mock_util().menu.return_value = (display_util.CANCEL, 0) self.assertTrue(self._call() is None) + + @test_util.patch_get_utility("certbot.plugins.selection.z_util") + def test_new_interaction_avoidance(self, mock_util): + mock_nginx = mock.Mock( + description_with_name="n", misconfigured=False) + mock_nginx.init().more_info.return_value = "nginx plugin" + mock_nginx.name = "nginx" + self.plugins[1] = mock_nginx + mock_util().menu.return_value = (display_util.CANCEL, 0) + + unset_cb_auto = os.environ.get("CERTBOT_AUTO") is None + if unset_cb_auto: + os.environ["CERTBOT_AUTO"] = "foo" + try: + self._call() + finally: + if unset_cb_auto: + del os.environ["CERTBOT_AUTO"] + + self.assertTrue("default" in mock_util().menu.call_args[1]) + +if __name__ == "__main__": + unittest.main() # pragma: no cover diff --git a/certbot/renewal.py b/certbot/renewal.py index 5c6697941..f3131c06f 100644 --- a/certbot/renewal.py +++ b/certbot/renewal.py @@ -389,14 +389,16 @@ def handle_renewal_request(config): disp = zope.component.getUtility(interfaces.IDisplay) disp.notification("Processing " + renewal_file, pause=False) lineage_config = copy.deepcopy(config) + lineagename = storage.lineagename_for_filename(renewal_file) # Note that this modifies config (to add back the configuration # elements from within the renewal configuration file). try: renewal_candidate = _reconstitute(lineage_config, renewal_file) except Exception as e: # pylint: disable=broad-except - logger.warning("Renewal configuration file %s produced an " - "unexpected error: %s. Skipping.", renewal_file, e) + logger.warning("Renewal configuration file %s (cert: %s) " + "produced an unexpected error: %s. Skipping.", + renewal_file, lineagename, e) logger.debug("Traceback was:\n%s", traceback.format_exc()) parse_failures.append(renewal_file) continue @@ -422,8 +424,9 @@ def handle_renewal_request(config): renew_skipped.append(renewal_candidate.fullchain) except Exception as e: # pylint: disable=broad-except # obtain_cert (presumably) encountered an unanticipated problem. - logger.warning("Attempting to renew cert from %s produced an " - "unexpected error: %s. Skipping.", renewal_file, e) + logger.warning("Attempting to renew cert (%s) from %s produced an " + "unexpected error: %s. Skipping.", lineagename, + renewal_file, e) logger.debug("Traceback was:\n%s", traceback.format_exc()) renew_failures.append(renewal_candidate.fullchain) diff --git a/certbot/storage.py b/certbot/storage.py index 4f167d4ea..d03052dae 100644 --- a/certbot/storage.py +++ b/certbot/storage.py @@ -186,8 +186,15 @@ def get_link_target(link): :returns: Absolute path to the target of link :rtype: str + :raises .CertStorageError: If link does not exists. + """ - target = os.readlink(link) + try: + target = os.readlink(link) + except OSError: + raise errors.CertStorageError( + "Expected {0} to be a symlink".format(link)) + if not os.path.isabs(target): target = os.path.join(os.path.dirname(link), target) return os.path.abspath(target) diff --git a/certbot/tests/account_test.py b/certbot/tests/account_test.py index 5950dcda9..575a40286 100644 --- a/certbot/tests/account_test.py +++ b/certbot/tests/account_test.py @@ -14,12 +14,10 @@ from acme import messages from certbot import errors -from certbot.tests import util +import certbot.tests.util as test_util -from certbot.tests.util import TempDirTestCase - -KEY = jose.JWKRSA.load(util.load_vector("rsa512_key_2.pem")) +KEY = jose.JWKRSA.load(test_util.load_vector("rsa512_key_2.pem")) class AccountTest(unittest.TestCase): @@ -58,12 +56,9 @@ class AccountTest(unittest.TestCase): self.assertTrue(repr(self.acc).startswith( "<Account(i_am_a_regr, bca5889f66457d5b62fbba7b25f9ab6f, Meta(")) -class ReportNewAccountTest(unittest.TestCase): +class ReportNewAccountTest(test_util.ConfigTestCase): """Tests for certbot.account.report_new_account.""" - def setUp(self): - self.config = mock.MagicMock(config_dir="/etc/letsencrypt") - def _call(self): from certbot.account import report_new_account report_new_account(self.config) @@ -98,14 +93,12 @@ class AccountMemoryStorageTest(unittest.TestCase): self.assertEqual([account], self.storage.find_all()) -class AccountFileStorageTest(TempDirTestCase): +class AccountFileStorageTest(test_util.ConfigTestCase): """Tests for certbot.account.AccountFileStorage.""" def setUp(self): super(AccountFileStorageTest, self).setUp() - self.config = mock.MagicMock( - accounts_dir=os.path.join(self.tempdir, "accounts")) from certbot.account import AccountFileStorage self.storage = AccountFileStorage(self.config) diff --git a/certbot/tests/cert_manager_test.py b/certbot/tests/cert_manager_test.py index f4a108019..401abcfbe 100644 --- a/certbot/tests/cert_manager_test.py +++ b/certbot/tests/cert_manager_test.py @@ -18,58 +18,50 @@ from certbot.storage import ALL_FOUR from certbot.tests import storage_test from certbot.tests import util as test_util -from certbot.tests.util import TempDirTestCase - -class BaseCertManagerTest(TempDirTestCase): +class BaseCertManagerTest(test_util.ConfigTestCase): """Base class for setting up Cert Manager tests. """ def setUp(self): super(BaseCertManagerTest, self).setUp() - os.makedirs(os.path.join(self.tempdir, "renewal")) - - self.cli_config = configuration.NamespaceConfig(mock.MagicMock( - config_dir=self.tempdir, - work_dir=self.tempdir, - logs_dir=self.tempdir, - quiet=False, - )) + self.config.quiet = False + os.makedirs(self.config.renewal_configs_dir) self.domains = { "example.org": None, - "other.com": os.path.join(self.tempdir, "specialarchive") + "other.com": os.path.join(self.config.config_dir, "specialarchive") } - self.configs = dict((domain, self._set_up_config(domain, self.domains[domain])) + self.config_files = dict((domain, self._set_up_config(domain, self.domains[domain])) for domain in self.domains) # We also create a file that isn't a renewal config in the same # location to test that logic that reads in all-and-only renewal # configs will ignore it and NOT attempt to parse it. - junk = open(os.path.join(self.tempdir, "renewal", "IGNORE.THIS"), "w") + junk = open(os.path.join(self.config.renewal_configs_dir, "IGNORE.THIS"), "w") junk.write("This file should be ignored!") junk.close() def _set_up_config(self, domain, custom_archive): # TODO: maybe provide NamespaceConfig.make_dirs? # TODO: main() should create those dirs, c.f. #902 - os.makedirs(os.path.join(self.tempdir, "live", domain)) - config = configobj.ConfigObj() + os.makedirs(os.path.join(self.config.live_dir, domain)) + config_file = configobj.ConfigObj() if custom_archive is not None: os.makedirs(custom_archive) - config["archive_dir"] = custom_archive + config_file["archive_dir"] = custom_archive else: - os.makedirs(os.path.join(self.tempdir, "archive", domain)) + os.makedirs(os.path.join(self.config.default_archive_dir, domain)) for kind in ALL_FOUR: - config[kind] = os.path.join(self.tempdir, "live", domain, + config_file[kind] = os.path.join(self.config.live_dir, domain, kind + ".pem") - config.filename = os.path.join(self.tempdir, "renewal", + config_file.filename = os.path.join(self.config.renewal_configs_dir, domain + ".conf") - config.write() - return config + config_file.write() + return config_file class UpdateLiveSymlinksTest(BaseCertManagerTest): @@ -86,27 +78,27 @@ class UpdateLiveSymlinksTest(BaseCertManagerTest): if custom_archive is not None: archive_dir_path = custom_archive else: - archive_dir_path = os.path.join(self.tempdir, "archive", domain) + archive_dir_path = os.path.join(self.config.default_archive_dir, domain) archive_paths[domain] = dict((kind, os.path.join(archive_dir_path, kind + "1.pem")) for kind in ALL_FOUR) for kind in ALL_FOUR: - live_path = self.configs[domain][kind] + live_path = self.config_files[domain][kind] archive_path = archive_paths[domain][kind] open(archive_path, 'a').close() # path is incorrect but base must be correct - os.symlink(os.path.join(self.tempdir, kind + "1.pem"), live_path) + os.symlink(os.path.join(self.config.config_dir, kind + "1.pem"), live_path) # run update symlinks - cert_manager.update_live_symlinks(self.cli_config) + cert_manager.update_live_symlinks(self.config) # check that symlinks go where they should prev_dir = os.getcwd() try: for domain in self.domains: for kind in ALL_FOUR: - os.chdir(os.path.dirname(self.configs[domain][kind])) + os.chdir(os.path.dirname(self.config_files[domain][kind])) self.assertEqual( - os.path.realpath(os.readlink(self.configs[domain][kind])), + os.path.realpath(os.readlink(self.config_files[domain][kind])), os.path.realpath(archive_paths[domain][kind])) finally: os.chdir(prev_dir) @@ -121,9 +113,9 @@ class DeleteTest(storage_test.BaseRenewableCertTest): def test_delete(self, mock_delete_files, mock_lineage_for_certname, unused_get_utility): """Test delete""" mock_lineage_for_certname.return_value = self.test_rc - self.cli_config.certname = "example.org" + self.config.certname = "example.org" from certbot import cert_manager - cert_manager.delete(self.cli_config) + cert_manager.delete(self.config) self.assertTrue(mock_delete_files.called) @@ -137,15 +129,15 @@ class CertificatesTest(BaseCertManagerTest): @mock.patch('certbot.cert_manager.logger') @test_util.patch_get_utility() def test_certificates_parse_fail(self, mock_utility, mock_logger): - self._certificates(self.cli_config) + self._certificates(self.config) self.assertTrue(mock_logger.warning.called) #pylint: disable=no-member self.assertTrue(mock_utility.called) @mock.patch('certbot.cert_manager.logger') @test_util.patch_get_utility() def test_certificates_quiet(self, mock_utility, mock_logger): - self.cli_config.quiet = True - self._certificates(self.cli_config) + self.config.quiet = True + self._certificates(self.config) self.assertFalse(mock_utility.notification.called) self.assertTrue(mock_logger.warning.called) #pylint: disable=no-member @@ -158,7 +150,7 @@ class CertificatesTest(BaseCertManagerTest): mock_utility, mock_logger, mock_verifier): mock_verifier.return_value = None mock_report.return_value = "" - self._certificates(self.cli_config) + self._certificates(self.config) self.assertFalse(mock_logger.warning.called) #pylint: disable=no-member self.assertTrue(mock_report.called) self.assertTrue(mock_utility.called) @@ -167,20 +159,19 @@ class CertificatesTest(BaseCertManagerTest): @mock.patch('certbot.cert_manager.logger') @test_util.patch_get_utility() def test_certificates_no_files(self, mock_utility, mock_logger): - tempdir = tempfile.mkdtemp() - - cli_config = configuration.NamespaceConfig(mock.MagicMock( - config_dir=tempdir, - work_dir=tempdir, - logs_dir=tempdir, - quiet=False, + empty_tempdir = tempfile.mkdtemp() + empty_config = configuration.NamespaceConfig(mock.MagicMock( + config_dir=os.path.join(empty_tempdir, "config"), + work_dir=os.path.join(empty_tempdir, "work"), + logs_dir=os.path.join(empty_tempdir, "logs"), + quiet=False )) - os.makedirs(os.path.join(tempdir, "renewal")) - self._certificates(cli_config) + os.makedirs(empty_config.renewal_configs_dir) + self._certificates(empty_config) self.assertFalse(mock_logger.warning.called) #pylint: disable=no-member self.assertTrue(mock_utility.called) - shutil.rmtree(tempdir) + shutil.rmtree(empty_tempdir) @mock.patch('certbot.cert_manager.ocsp.RevocationChecker.ocsp_revoked') def test_report_human_readable(self, mock_revoked): @@ -261,7 +252,7 @@ class SearchLineagesTest(BaseCertManagerTest): mock_renewable_cert.side_effect = errors.CertStorageError from certbot import cert_manager # pylint: disable=protected-access - self.assertEqual(cert_manager._search_lineages(self.cli_config, lambda x: x, "check"), + self.assertEqual(cert_manager._search_lineages(self.config, lambda x: x, "check"), "check") self.assertTrue(mock_make_or_verify_dir.called) @@ -278,7 +269,7 @@ class LineageForCertnameTest(BaseCertManagerTest): mock_match = mock.Mock(lineagename="example.com") mock_renewable_cert.return_value = mock_match from certbot import cert_manager - self.assertEqual(cert_manager.lineage_for_certname(self.cli_config, "example.com"), + self.assertEqual(cert_manager.lineage_for_certname(self.config, "example.com"), mock_match) self.assertTrue(mock_make_or_verify_dir.called) @@ -288,7 +279,7 @@ class LineageForCertnameTest(BaseCertManagerTest): mock_make_or_verify_dir): mock_renewal_conf_file.return_value = "other.com.conf" from certbot import cert_manager - self.assertEqual(cert_manager.lineage_for_certname(self.cli_config, "example.com"), + self.assertEqual(cert_manager.lineage_for_certname(self.config, "example.com"), None) self.assertTrue(mock_make_or_verify_dir.called) @@ -298,7 +289,7 @@ class LineageForCertnameTest(BaseCertManagerTest): mock_make_or_verify_dir): mock_renewal_conf_file.side_effect = errors.CertStorageError() from certbot import cert_manager - self.assertEqual(cert_manager.lineage_for_certname(self.cli_config, "example.com"), + self.assertEqual(cert_manager.lineage_for_certname(self.config, "example.com"), None) self.assertTrue(mock_make_or_verify_dir.called) @@ -317,7 +308,7 @@ class DomainsForCertnameTest(BaseCertManagerTest): mock_match.names.return_value = domains mock_renewable_cert.return_value = mock_match from certbot import cert_manager - self.assertEqual(cert_manager.domains_for_certname(self.cli_config, "example.com"), + self.assertEqual(cert_manager.domains_for_certname(self.config, "example.com"), domains) self.assertTrue(mock_make_or_verify_dir.called) @@ -327,7 +318,7 @@ class DomainsForCertnameTest(BaseCertManagerTest): mock_make_or_verify_dir): mock_renewal_conf_file.return_value = "somefile.conf" from certbot import cert_manager - self.assertEqual(cert_manager.domains_for_certname(self.cli_config, "other.com"), + self.assertEqual(cert_manager.domains_for_certname(self.config, "other.com"), None) self.assertTrue(mock_make_or_verify_dir.called) @@ -337,15 +328,8 @@ class RenameLineageTest(BaseCertManagerTest): def setUp(self): super(RenameLineageTest, self).setUp() - self.mock_config = configuration.NamespaceConfig( - namespace=mock.MagicMock( - config_dir=self.tempdir, - work_dir=self.tempdir, - logs_dir=self.tempdir, - certname="example.org", - new_certname="after", - ) - ) + self.config.certname = "example.org" + self.config.new_certname = "after" def _call(self, *args, **kwargs): from certbot import cert_manager @@ -354,81 +338,81 @@ class RenameLineageTest(BaseCertManagerTest): @mock.patch('certbot.storage.renewal_conf_files') @test_util.patch_get_utility() def test_no_certname(self, mock_get_utility, mock_renewal_conf_files): - mock_config = mock.Mock(certname=None, new_certname="two") + self.config.certname = None + self.config.new_certname = "two" # if not choices mock_renewal_conf_files.return_value = [] - self.assertRaises(errors.Error, self._call, mock_config) + self.assertRaises(errors.Error, self._call, self.config) mock_renewal_conf_files.return_value = ["one.conf"] util_mock = mock.Mock() util_mock.menu.return_value = (display_util.CANCEL, 0) mock_get_utility.return_value = util_mock - self.assertRaises(errors.Error, self._call, mock_config) + self.assertRaises(errors.Error, self._call, self.config) util_mock.menu.return_value = (display_util.OK, -1) - self.assertRaises(errors.Error, self._call, mock_config) + self.assertRaises(errors.Error, self._call, self.config) @test_util.patch_get_utility() def test_no_new_certname(self, mock_get_utility): - mock_config = mock.Mock(certname="one", new_certname=None) + self.config.certname = "one" + self.config.new_certname = None util_mock = mock.Mock() util_mock.input.return_value = (display_util.CANCEL, "name") mock_get_utility.return_value = util_mock - self.assertRaises(errors.Error, self._call, mock_config) + self.assertRaises(errors.Error, self._call, self.config) util_mock = mock.Mock() util_mock.input.return_value = (display_util.OK, None) mock_get_utility.return_value = util_mock - self.assertRaises(errors.Error, self._call, mock_config) + self.assertRaises(errors.Error, self._call, self.config) @test_util.patch_get_utility() @mock.patch('certbot.cert_manager.lineage_for_certname') def test_no_existing_certname(self, mock_lineage_for_certname, unused_get_utility): - mock_config = mock.Mock(certname="one", new_certname="two") + self.config.certname = "one" + self.config.new_certname = "two" mock_lineage_for_certname.return_value = None self.assertRaises(errors.ConfigurationError, - self._call, mock_config) + self._call, self.config) @test_util.patch_get_utility() @mock.patch("certbot.storage.RenewableCert._check_symlinks") def test_rename_cert(self, mock_check, unused_get_utility): mock_check.return_value = True - mock_config = self.mock_config - self._call(mock_config) + self._call(self.config) from certbot import cert_manager - updated_lineage = cert_manager.lineage_for_certname(mock_config, mock_config.new_certname) + updated_lineage = cert_manager.lineage_for_certname(self.config, self.config.new_certname) self.assertTrue(updated_lineage is not None) - self.assertEqual(updated_lineage.lineagename, mock_config.new_certname) + self.assertEqual(updated_lineage.lineagename, self.config.new_certname) @test_util.patch_get_utility() @mock.patch("certbot.storage.RenewableCert._check_symlinks") def test_rename_cert_interactive_certname(self, mock_check, mock_get_utility): mock_check.return_value = True - mock_config = self.mock_config - mock_config.certname = None + self.config.certname = None util_mock = mock.Mock() util_mock.menu.return_value = (display_util.OK, 0) mock_get_utility.return_value = util_mock - self._call(mock_config) + self._call(self.config) from certbot import cert_manager - updated_lineage = cert_manager.lineage_for_certname(mock_config, mock_config.new_certname) + updated_lineage = cert_manager.lineage_for_certname(self.config, self.config.new_certname) self.assertTrue(updated_lineage is not None) - self.assertEqual(updated_lineage.lineagename, mock_config.new_certname) + self.assertEqual(updated_lineage.lineagename, self.config.new_certname) @test_util.patch_get_utility() @mock.patch("certbot.storage.RenewableCert._check_symlinks") def test_rename_cert_bad_new_certname(self, mock_check, unused_get_utility): mock_check.return_value = True - mock_config = self.mock_config # for example, don't rename to existing certname - mock_config.new_certname = "example.org" - self.assertRaises(errors.ConfigurationError, self._call, mock_config) + self.config.new_certname = "example.org" + self.assertRaises(errors.ConfigurationError, self._call, self.config) - mock_config.new_certname = "one{0}two".format(os.path.sep) - self.assertRaises(errors.ConfigurationError, self._call, mock_config) + self.config.new_certname = "one{0}two".format(os.path.sep) + self.assertRaises(errors.ConfigurationError, self._call, self.config) class DuplicativeCertsTest(storage_test.BaseRenewableCertTest): @@ -436,7 +420,7 @@ class DuplicativeCertsTest(storage_test.BaseRenewableCertTest): def setUp(self): super(DuplicativeCertsTest, self).setUp() - self.config.write() + self.config_file.write() self._write_out_ex_kinds() @mock.patch('certbot.util.make_or_verify_dir') @@ -448,24 +432,24 @@ class DuplicativeCertsTest(storage_test.BaseRenewableCertTest): # No overlap at all result = find_duplicative_certs( - self.cli_config, ['wow.net', 'hooray.org']) + self.config, ['wow.net', 'hooray.org']) self.assertEqual(result, (None, None)) # Totally identical result = find_duplicative_certs( - self.cli_config, ['example.com', 'www.example.com']) + self.config, ['example.com', 'www.example.com']) self.assertTrue(result[0].configfile.filename.endswith('example.org.conf')) self.assertEqual(result[1], None) # Superset result = find_duplicative_certs( - self.cli_config, ['example.com', 'www.example.com', 'something.new']) + self.config, ['example.com', 'www.example.com', 'something.new']) self.assertEqual(result[0], None) self.assertTrue(result[1].configfile.filename.endswith('example.org.conf')) # Partial overlap doesn't count result = find_duplicative_certs( - self.cli_config, ['example.com', 'something.new']) + self.config, ['example.com', 'something.new']) self.assertEqual(result, (None, None)) diff --git a/certbot/tests/cli_test.py b/certbot/tests/cli_test.py index f18da240a..108e07eb3 100644 --- a/certbot/tests/cli_test.py +++ b/certbot/tests/cli_test.py @@ -40,7 +40,7 @@ class TestReadFile(TempDirTestCase): -class ParseTest(unittest.TestCase): +class ParseTest(unittest.TestCase): # pylint: disable=too-many-public-methods '''Test the cli args entrypoint''' _multiprocess_can_split_ = True @@ -109,6 +109,7 @@ class ParseTest(unittest.TestCase): self.assertTrue("--dialog" not in out) self.assertTrue("%s" not in out) self.assertTrue("{0}" not in out) + self.assertTrue("--renew-hook" not in out) out = self._help_output(['-h', 'nginx']) if "nginx" in PLUGINS: @@ -323,6 +324,58 @@ class ParseTest(unittest.TestCase): self.assertRaises( errors.Error, self.parse, "-n --force-interactive".split()) + def test_deploy_hook_conflict(self): + with mock.patch("certbot.cli.sys.stderr"): + self.assertRaises(SystemExit, self.parse, + "--renew-hook foo --deploy-hook bar".split()) + + def test_deploy_hook_matches_renew_hook(self): + value = "foo" + namespace = self.parse(["--renew-hook", value, + "--deploy-hook", value, + "--disable-hook-validation"]) + self.assertEqual(namespace.deploy_hook, value) + self.assertEqual(namespace.renew_hook, value) + + def test_deploy_hook_sets_renew_hook(self): + value = "foo" + namespace = self.parse( + ["--deploy-hook", value, "--disable-hook-validation"]) + self.assertEqual(namespace.deploy_hook, value) + self.assertEqual(namespace.renew_hook, value) + + def test_renew_hook_conflict(self): + with mock.patch("certbot.cli.sys.stderr"): + self.assertRaises(SystemExit, self.parse, + "--deploy-hook foo --renew-hook bar".split()) + + def test_renew_hook_matches_deploy_hook(self): + value = "foo" + namespace = self.parse(["--deploy-hook", value, + "--renew-hook", value, + "--disable-hook-validation"]) + self.assertEqual(namespace.deploy_hook, value) + self.assertEqual(namespace.renew_hook, value) + + def test_renew_hook_does_not_set_renew_hook(self): + value = "foo" + namespace = self.parse( + ["--renew-hook", value, "--disable-hook-validation"]) + self.assertEqual(namespace.deploy_hook, None) + self.assertEqual(namespace.renew_hook, value) + + def test_max_log_backups_error(self): + with mock.patch('certbot.cli.sys.stderr'): + self.assertRaises( + SystemExit, self.parse, "--max-log-backups foo".split()) + self.assertRaises( + SystemExit, self.parse, "--max-log-backups -42".split()) + + def test_max_log_backups_success(self): + value = "42" + namespace = self.parse(["--max-log-backups", value]) + self.assertEqual(namespace.max_log_backups, int(value)) + class DefaultTest(unittest.TestCase): """Tests for certbot.cli._Default.""" diff --git a/certbot/tests/client_test.py b/certbot/tests/client_test.py index 97fd6241d..a547f6a9a 100644 --- a/certbot/tests/client_test.py +++ b/certbot/tests/client_test.py @@ -21,20 +21,13 @@ KEY = test_util.load_vector("rsa512_key.pem") CSR_SAN = test_util.load_vector("csr-san.pem") -class ConfigHelper(object): - """Creates a dummy object to imitate a namespace object - - Example: cfg = ConfigHelper(redirect=True, hsts=False, uir=False) - will result in: cfg.redirect=True, cfg.hsts=False, etc. - """ - def __init__(self, **kwds): - self.__dict__.update(kwds) - -class RegisterTest(unittest.TestCase): +class RegisterTest(test_util.ConfigTestCase): """Tests for certbot.client.register.""" def setUp(self): - self.config = mock.MagicMock(rsa_key_size=1024, register_unsafely_without_email=False) + super(RegisterTest, self).setUp() + self.config.rsa_key_size = 1024 + self.config.register_unsafely_without_email = False self.account_storage = account.AccountMemoryStorage() self.tos_cb = mock.MagicMock() @@ -116,14 +109,12 @@ class RegisterTest(unittest.TestCase): self.assertFalse(mock_handle.called) -class ClientTestCommon(unittest.TestCase): +class ClientTestCommon(test_util.ConfigTestCase): """Common base class for certbot.client.Client tests.""" def setUp(self): - self.config = mock.MagicMock( - no_verify_ssl=False, - config_dir="/etc/letsencrypt", - work_dir="/var/lib/letsencrypt", - allow_subset_of_names=False) + super(ClientTestCommon, self).setUp() + self.config.no_verify_ssl = False + self.config.allow_subset_of_names = False # pylint: disable=star-args self.account = mock.MagicMock(**{"key.pem": KEY}) @@ -143,7 +134,6 @@ class ClientTest(ClientTestCommon): super(ClientTest, self).setUp() self.config.allow_subset_of_names = False - self.config.config_dir = "/etc/letsencrypt" self.config.dry_run = False self.eg_domains = ["example.com", "www.example.com"] @@ -262,8 +252,8 @@ class ClientTest(ClientTestCommon): mock_crypto.make_key.return_value = mock.sentinel.key_pem key = util.Key(file=None, pem=mock.sentinel.key_pem) - with mock.patch.object(self.client.config, 'dry_run', new=True): - self._test_obtain_certificate_common(key, csr) + self.client.config.dry_run = True + self._test_obtain_certificate_common(key, csr) mock_crypto.make_key.assert_called_once_with(self.config.rsa_key_size) mock_acme_crypto.make_csr.assert_called_once_with( diff --git a/certbot/tests/configuration_test.py b/certbot/tests/configuration_test.py index 66a07dddd..eafee6339 100644 --- a/certbot/tests/configuration_test.py +++ b/certbot/tests/configuration_test.py @@ -6,33 +6,32 @@ import mock from certbot import errors +from certbot.tests import util as test_util -class NamespaceConfigTest(unittest.TestCase): +class NamespaceConfigTest(test_util.ConfigTestCase): """Tests for certbot.configuration.NamespaceConfig.""" def setUp(self): - self.namespace = mock.MagicMock( - config_dir='/tmp/config', work_dir='/tmp/foo', - logs_dir="/tmp/bar", foo='bar', - server='https://acme-server.org:443/new', - tls_sni_01_port=1234, http01_port=4321) - from certbot.configuration import NamespaceConfig - self.config = NamespaceConfig(self.namespace) + super(NamespaceConfigTest, self).setUp() + self.config.foo = 'bar' + self.config.server = 'https://acme-server.org:443/new' + self.config.tls_sni_01_port = 1234 + self.config.http01_port = 4321 def test_init_same_ports(self): - self.namespace.tls_sni_01_port = 4321 + self.config.namespace.tls_sni_01_port = 4321 from certbot.configuration import NamespaceConfig - self.assertRaises(errors.Error, NamespaceConfig, self.namespace) + self.assertRaises(errors.Error, NamespaceConfig, self.config.namespace) def test_proxy_getattr(self): self.assertEqual(self.config.foo, 'bar') - self.assertEqual(self.config.work_dir, '/tmp/foo') + self.assertEqual(self.config.work_dir, os.path.join(self.tempdir, 'work')) def test_server_path(self): self.assertEqual(['acme-server.org:443', 'new'], self.config.server_path.split(os.path.sep)) - self.namespace.server = ('http://user:pass@acme.server:443' + self.config.namespace.server = ('http://user:pass@acme.server:443' '/p/a/t/h;parameters?query#fragment') self.assertEqual(['user:pass@acme.server:443', 'p', 'a', 't', 'h'], self.config.server_path.split(os.path.sep)) @@ -48,12 +47,18 @@ class NamespaceConfigTest(unittest.TestCase): constants.TEMP_CHECKPOINT_DIR = 't' self.assertEqual( - self.config.accounts_dir, '/tmp/config/acc/acme-server.org:443/new') - self.assertEqual(self.config.backup_dir, '/tmp/foo/backups') - self.assertEqual(self.config.csr_dir, '/tmp/config/csr') - self.assertEqual(self.config.in_progress_dir, '/tmp/foo/../p') - self.assertEqual(self.config.key_dir, '/tmp/config/keys') - self.assertEqual(self.config.temp_checkpoint_dir, '/tmp/foo/t') + self.config.accounts_dir, os.path.join( + self.config.config_dir, 'acc/acme-server.org:443/new')) + self.assertEqual( + self.config.backup_dir, os.path.join(self.config.work_dir, 'backups')) + self.assertEqual( + self.config.csr_dir, os.path.join(self.config.config_dir, 'csr')) + self.assertEqual( + self.config.in_progress_dir, os.path.join(self.config.work_dir, '../p')) + self.assertEqual( + self.config.key_dir, os.path.join(self.config.config_dir, 'keys')) + self.assertEqual( + self.config.temp_checkpoint_dir, os.path.join(self.config.work_dir, 't')) def test_absolute_paths(self): from certbot.configuration import NamespaceConfig @@ -95,10 +100,13 @@ class NamespaceConfigTest(unittest.TestCase): constants.LIVE_DIR = 'l' constants.RENEWAL_CONFIGS_DIR = 'renewal_configs' - self.assertEqual(self.config.default_archive_dir, '/tmp/config/a') - self.assertEqual(self.config.live_dir, '/tmp/config/l') self.assertEqual( - self.config.renewal_configs_dir, '/tmp/config/renewal_configs') + self.config.default_archive_dir, os.path.join(self.config.config_dir, 'a')) + self.assertEqual( + self.config.live_dir, os.path.join(self.config.config_dir, 'l')) + self.assertEqual( + self.config.renewal_configs_dir, os.path.join( + self.config.config_dir, 'renewal_configs')) def test_renewal_absolute_paths(self): from certbot.configuration import NamespaceConfig diff --git a/certbot/tests/crypto_util_test.py b/certbot/tests/crypto_util_test.py index 729b09dc1..bc41ca950 100644 --- a/certbot/tests/crypto_util_test.py +++ b/certbot/tests/crypto_util_test.py @@ -252,9 +252,11 @@ class VerifyCertMatchesPrivKeyTest(VerifyCertSetup): def _call(self, renewable_cert): from certbot.crypto_util import verify_cert_matches_priv_key - return verify_cert_matches_priv_key(renewable_cert) + return verify_cert_matches_priv_key(renewable_cert.cert, renewable_cert.privkey) def test_cert_priv_key_match(self): + self.renewable_cert.cert = SS_CERT_PATH + self.renewable_cert.privkey = RSA2048_KEY_PATH self.assertEqual(None, self._call(self.renewable_cert)) def test_cert_priv_key_mismatch(self): diff --git a/certbot/tests/eff_test.py b/certbot/tests/eff_test.py index fd9a61181..160af1993 100644 --- a/certbot/tests/eff_test.py +++ b/certbot/tests/eff_test.py @@ -4,20 +4,22 @@ import unittest import mock from certbot import constants -from certbot.tests import util +import certbot.tests.util as test_util -class HandleSubscriptionTest(unittest.TestCase): +class HandleSubscriptionTest(test_util.ConfigTestCase): """Tests for certbot.eff.handle_subscription.""" def setUp(self): + super(HandleSubscriptionTest, self).setUp() self.email = 'certbot@example.org' - self.config = mock.Mock(email=self.email, eff_email=None) + self.config.email = self.email + self.config.eff_email = None def _call(self): from certbot.eff import handle_subscription return handle_subscription(self.config) - @util.patch_get_utility() + @test_util.patch_get_utility() @mock.patch('certbot.eff.subscribe') def test_failure(self, mock_subscribe, mock_get_utility): self.config.email = None @@ -32,12 +34,12 @@ class HandleSubscriptionTest(unittest.TestCase): @mock.patch('certbot.eff.subscribe') def test_no_subscribe_with_no_prompt(self, mock_subscribe): self.config.eff_email = False - with util.patch_get_utility() as mock_get_utility: + with test_util.patch_get_utility() as mock_get_utility: self._call() self.assertFalse(mock_subscribe.called) self._assert_no_get_utility_calls(mock_get_utility) - @util.patch_get_utility() + @test_util.patch_get_utility() @mock.patch('certbot.eff.subscribe') def test_subscribe_with_no_prompt(self, mock_subscribe, mock_get_utility): self.config.eff_email = True @@ -49,7 +51,7 @@ class HandleSubscriptionTest(unittest.TestCase): self.assertFalse(mock_get_utility().yesno.called) self.assertFalse(mock_get_utility().add_message.called) - @util.patch_get_utility() + @test_util.patch_get_utility() @mock.patch('certbot.eff.subscribe') def test_subscribe_with_prompt(self, mock_subscribe, mock_get_utility): mock_get_utility().yesno.return_value = True @@ -62,7 +64,7 @@ class HandleSubscriptionTest(unittest.TestCase): self.assertTrue(mock_subscribe.called) self.assertEqual(mock_subscribe.call_args[0][0], self.email) - @util.patch_get_utility() + @test_util.patch_get_utility() @mock.patch('certbot.eff.subscribe') def test_no_subscribe_with_prompt(self, mock_subscribe, mock_get_utility): mock_get_utility().yesno.return_value = False @@ -105,7 +107,7 @@ class SubscribeTest(unittest.TestCase): self.assertFalse(data is None) self.assertEqual(data.get('email'), self.email) - @util.patch_get_utility() + @test_util.patch_get_utility() def test_bad_status(self, mock_get_utility): self.json['status'] = False self._call() # pylint: disable=no-value-for-parameter @@ -113,7 +115,7 @@ class SubscribeTest(unittest.TestCase): expected_part = 'because your e-mail address appears to be invalid.' self.assertTrue(expected_part in actual) - @util.patch_get_utility() + @test_util.patch_get_utility() def test_not_ok(self, mock_get_utility): self.response.ok = False self._call() # pylint: disable=no-value-for-parameter @@ -125,7 +127,7 @@ class SubscribeTest(unittest.TestCase): self.assertTrue(mock_get_utility().add_message.called) return mock_get_utility().add_message.call_args[0][0] - @util.patch_get_utility() + @test_util.patch_get_utility() def test_subscribe(self, mock_get_utility): self._call() # pylint: disable=no-value-for-parameter self.assertFalse(mock_get_utility.called) diff --git a/certbot/tests/errors_test.py b/certbot/tests/errors_test.py index aee1857a6..c8a6c4ac5 100644 --- a/certbot/tests/errors_test.py +++ b/certbot/tests/errors_test.py @@ -23,6 +23,17 @@ class FailedChallengesTest(unittest.TestCase): self.assertTrue(str(self.error).startswith( "Failed authorization procedure. example.com (dns-01): tls")) + def test_unicode(self): + from certbot.errors import FailedChallenges + arabic_detail = u'\u0639\u062f\u0627\u0644\u0629' + arabic_error = FailedChallenges(set([achallenges.DNS( + domain="example.com", challb=messages.ChallengeBody( + chall=acme_util.DNS01, uri=None, + error=messages.Error(typ="tls", detail=arabic_detail)))])) + + self.assertTrue(str(arabic_error).startswith( + "Failed authorization procedure. example.com (dns-01): tls")) + class StandaloneBindErrorTest(unittest.TestCase): """Tests for certbot.errors.StandaloneBindError.""" diff --git a/certbot/tests/hook_test.py b/certbot/tests/hook_test.py index 0fbb91492..964d69866 100644 --- a/certbot/tests/hook_test.py +++ b/certbot/tests/hook_test.py @@ -16,7 +16,8 @@ class HookTest(unittest.TestCase): @mock.patch('certbot.hooks._prog') def test_validate_hooks(self, mock_prog): - config = mock.MagicMock(pre_hook="", post_hook="ls -lR", renew_hook="uptime") + config = mock.MagicMock(deploy_hook=None, pre_hook="", + post_hook="ls -lR", renew_hook="uptime") hooks.validate_hooks(config) self.assertEqual(mock_prog.call_count, 2) self.assertEqual(mock_prog.call_args_list[1][0][0], 'uptime') @@ -25,6 +26,19 @@ class HookTest(unittest.TestCase): config = mock.MagicMock(pre_hook="explodinator", post_hook="", renew_hook="") self.assertRaises(errors.HookCommandNotFound, hooks.validate_hooks, config) + @mock.patch('certbot.hooks.validate_hook') + def test_validation_order(self, mock_validate_hook): + # This ensures error messages are about deploy hook when appropriate + config = mock.Mock(deploy_hook=None, pre_hook=None, + post_hook=None, renew_hook=None) + hooks.validate_hooks(config) + + order = [call[0][1] for call in mock_validate_hook.call_args_list] + self.assertTrue('pre' in order) + self.assertTrue('post' in order) + self.assertTrue('deploy' in order) + self.assertEqual(order[-1], 'renew') + @mock.patch('certbot.hooks.util.exe_exists') @mock.patch('certbot.hooks.plug_util.path_surgery') def test_prog(self, mock_ps, mock_exe_exists): @@ -35,6 +49,19 @@ class HookTest(unittest.TestCase): self.assertEqual(hooks._prog("funky"), None) self.assertEqual(mock_ps.call_count, 1) + @mock.patch('certbot.hooks.renew_hook') + def test_deploy_hook(self, mock_renew_hook): + args = (mock.Mock(deploy_hook='foo'), ['example.org'], 'path',) + # pylint: disable=star-args + hooks.deploy_hook(*args) + mock_renew_hook.assert_called_once_with(*args) + + @mock.patch('certbot.hooks.renew_hook') + def test_no_deploy_hook(self, mock_renew_hook): + args = (mock.Mock(deploy_hook=None), ['example.org'], 'path',) + hooks.deploy_hook(*args) # pylint: disable=star-args + mock_renew_hook.assert_not_called() + def _test_a_hook(self, config, hook_function, calls_expected, **kwargs): with mock.patch('certbot.hooks.logger') as mock_logger: mock_logger.warning = mock.MagicMock() diff --git a/certbot/tests/log_test.py b/certbot/tests/log_test.py index 72ff076dd..1f2dc3be0 100644 --- a/certbot/tests/log_test.py +++ b/certbot/tests/log_test.py @@ -55,7 +55,7 @@ class PreArgParseSetupTest(unittest.TestCase): memory_handler, 1, 2, 3, debug=True, log_path=mock.ANY) -class PostArgParseSetupTest(test_util.TempDirTestCase): +class PostArgParseSetupTest(test_util.ConfigTestCase): """Tests for certbot.log.post_arg_parse_setup.""" @classmethod @@ -65,9 +65,10 @@ class PostArgParseSetupTest(test_util.TempDirTestCase): def setUp(self): super(PostArgParseSetupTest, self).setUp() - self.config = mock.MagicMock( - debug=False, logs_dir=self.tempdir, quiet=False, - verbose_count=constants.CLI_DEFAULTS['verbose_count']) + self.config.debug = False + self.config.max_log_backups = 1000 + self.config.quiet = False + self.config.verbose_count = constants.CLI_DEFAULTS['verbose_count'] self.devnull = open(os.devnull, 'w') from certbot.log import ColoredStreamHandler @@ -102,7 +103,7 @@ class PostArgParseSetupTest(test_util.TempDirTestCase): self.assertFalse(os.path.exists(self.temp_path)) mock_sys.excepthook(1, 2, 3) mock_except_hook.assert_called_once_with( - 1, 2, 3, debug=self.config.debug, log_path=self.tempdir) + 1, 2, 3, debug=self.config.debug, log_path=self.config.logs_dir) level = self.stream_handler.level if self.config.quiet: @@ -119,7 +120,7 @@ class PostArgParseSetupTest(test_util.TempDirTestCase): self.test_common() -class SetupLogFileHandlerTest(test_util.TempDirTestCase): +class SetupLogFileHandlerTest(test_util.ConfigTestCase): """Tests for certbot.log.setup_log_file_handler.""" @classmethod @@ -129,7 +130,7 @@ class SetupLogFileHandlerTest(test_util.TempDirTestCase): def setUp(self): super(SetupLogFileHandlerTest, self).setUp() - self.config = mock.MagicMock(logs_dir=self.tempdir) + self.config.max_log_backups = 42 @mock.patch('certbot.main.logging.handlers.RotatingFileHandler') def test_failure(self, mock_handler): @@ -142,15 +143,32 @@ class SetupLogFileHandlerTest(test_util.TempDirTestCase): else: # pragma: no cover self.fail('Error not raised.') - def test_success(self): + def test_success_with_rollover(self): + self._test_success_common(should_rollover=True) + + def test_success_without_rollover(self): + self.config.max_log_backups = 0 + self._test_success_common(should_rollover=False) + + def _test_success_common(self, should_rollover): log_file = 'test.log' handler, log_path = self._call(self.config, log_file, '%(message)s') + handler.close() + self.assertEqual(handler.level, logging.DEBUG) self.assertEqual(handler.formatter.converter, time.gmtime) expected_path = os.path.join(self.config.logs_dir, log_file) self.assertEqual(log_path, expected_path) - handler.close() + + backup_path = os.path.join(self.config.logs_dir, log_file + '.1') + self.assertEqual(os.path.exists(backup_path), should_rollover) + + @mock.patch('certbot.log.logging.handlers.RotatingFileHandler') + def test_max_log_backups_used(self, mock_handler): + self._call(self.config, 'test.log', '%(message)s') + backup_count = mock_handler.call_args[1]['backupCount'] + self.assertEqual(self.config.max_log_backups, backup_count) class ColoredStreamHandlerTest(unittest.TestCase): diff --git a/certbot/tests/main_test.py b/certbot/tests/main_test.py index 7c2016178..ae34ebfd7 100644 --- a/certbot/tests/main_test.py +++ b/certbot/tests/main_test.py @@ -23,8 +23,6 @@ from certbot import configuration from certbot import crypto_util from certbot import errors from certbot import main -from certbot import renewal -from certbot import storage from certbot import util from certbot.plugins import disco @@ -37,6 +35,8 @@ CERT = test_util.vector_path('cert.pem') CSR = test_util.vector_path('csr.der') KEY = test_util.vector_path('rsa256_key.pem') JWK = jose.JWKRSA.load(test_util.load_vector("rsa512_key_2.pem")) +RSA2048_KEY_PATH = test_util.vector_path('rsa2048_key.pem') +SS_CERT_PATH = test_util.vector_path('self_signed_cert.pem') class TestHandleIdenticalCerts(unittest.TestCase): @@ -287,16 +287,14 @@ class RevokeTest(test_util.TempDirTestCase): self.mock_success_revoke.assert_not_called() -class DetermineAccountTest(unittest.TestCase): +class DetermineAccountTest(test_util.ConfigTestCase): """Tests for certbot.main._determine_account.""" def setUp(self): - self.args = mock.MagicMock(account=None, email=None, - config_dir="unused_config", - logs_dir="unused_logs", - work_dir="unused_work", - register_unsafely_without_email=False) - self.config = configuration.NamespaceConfig(self.args) + super(DetermineAccountTest, self).setUp() + self.config.account = None + self.config.email = None + self.config.register_unsafely_without_email = False self.accs = [mock.MagicMock(id='x'), mock.MagicMock(id='y')] self.account_storage = account.AccountMemoryStorage() # For use in saving accounts: fake out the new_authz URL. @@ -357,19 +355,16 @@ class DetermineAccountTest(unittest.TestCase): self.assertEqual('other email', self.config.email) -class MainTest(test_util.TempDirTestCase): # pylint: disable=too-many-public-methods +class MainTest(test_util.ConfigTestCase): # pylint: disable=too-many-public-methods """Tests for different commands.""" def setUp(self): super(MainTest, self).setUp() - self.config_dir = os.path.join(self.tempdir, 'config') - self.work_dir = os.path.join(self.tempdir, 'work') - self.logs_dir = os.path.join(self.tempdir, 'logs') - os.mkdir(self.logs_dir) - self.standard_args = ['--config-dir', self.config_dir, - '--work-dir', self.work_dir, - '--logs-dir', self.logs_dir, '--text'] + os.mkdir(self.config.logs_dir) + self.standard_args = ['--config-dir', self.config.config_dir, + '--work-dir', self.config.work_dir, + '--logs-dir', self.config.logs_dir, '--text'] def tearDown(self): # Reset globals in cli @@ -678,11 +673,12 @@ class MainTest(test_util.TempDirTestCase): # pylint: disable=too-many-public-me @test_util.patch_get_utility() def test_certonly_new_request_success(self, mock_get_utility, mock_notAfter): cert_path = '/etc/letsencrypt/live/foo.bar' + key_path = '/etc/letsencrypt/live/baz.qux' date = '1970-01-01' mock_notAfter().date.return_value = date mock_lineage = mock.MagicMock(cert=cert_path, fullchain=cert_path, - fullchain_path=cert_path) + fullchain_path=cert_path, key_path=key_path) mock_client = mock.MagicMock() mock_client.obtain_and_enroll_certificate.return_value = mock_lineage self._certonly_new_request_common(mock_client) @@ -691,6 +687,7 @@ class MainTest(test_util.TempDirTestCase): # pylint: disable=too-many-public-me cert_msg = mock_get_utility().add_message.call_args_list[0][0][0] self.assertTrue(cert_path in cert_msg) self.assertTrue(date in cert_msg) + self.assertTrue(key_path in cert_msg) self.assertTrue( 'donate' in mock_get_utility().add_message.call_args[0][0]) @@ -752,7 +749,7 @@ class MainTest(test_util.TempDirTestCase): # pylint: disable=too-many-public-me raise finally: if log_out: - with open(os.path.join(self.logs_dir, "letsencrypt.log")) as lf: + with open(os.path.join(self.config.logs_dir, "letsencrypt.log")) as lf: self.assertTrue(log_out in lf.read()) return mock_lineage, mock_get_utility, stdout @@ -784,18 +781,18 @@ class MainTest(test_util.TempDirTestCase): # pylint: disable=too-many-public-me def _dump_log(self): print("Logs:") - log_path = os.path.join(self.logs_dir, "letsencrypt.log") + log_path = os.path.join(self.config.logs_dir, "letsencrypt.log") if os.path.exists(log_path): with open(log_path) as lf: print(lf.read()) def test_renew_verb(self): - test_util.make_lineage(self.config_dir, 'sample-renewal.conf') + test_util.make_lineage(self.config.config_dir, 'sample-renewal.conf') args = ["renew", "--dry-run", "-tvv"] self._test_renewal_common(True, [], args=args, should_renew=True) def test_quiet_renew(self): - test_util.make_lineage(self.config_dir, 'sample-renewal.conf') + test_util.make_lineage(self.config.config_dir, 'sample-renewal.conf') args = ["renew", "--dry-run"] _, _, stdout = self._test_renewal_common(True, [], args=args, should_renew=True) out = stdout.getvalue() @@ -807,36 +804,21 @@ class MainTest(test_util.TempDirTestCase): # pylint: disable=too-many-public-me self.assertEqual("", out) def test_renew_hook_validation(self): - test_util.make_lineage(self.config_dir, 'sample-renewal.conf') + test_util.make_lineage(self.config.config_dir, '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): - test_util.make_lineage(self.config_dir, 'sample-renewal.conf') + test_util.make_lineage(self.config.config_dir, 'sample-renewal.conf') args = ["renew", "--dry-run", "--post-hook=no-such-command", "--disable-hook-validation"] with mock.patch("certbot.hooks.post_hook"): 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): - mock_set_by_cli.return_value = False - rc_path = test_util.make_lineage( - self.config_dir, 'sample-renewal-ancient.conf') - args = mock.MagicMock(account=None, config_dir=self.config_dir, - logs_dir=self.logs_dir, work_dir=self.work_dir, - email=None, webroot_path=None) - config = configuration.NamespaceConfig(args) - lineage = storage.RenewableCert(rc_path, config) - renewalparams = lineage.configuration["renewalparams"] - # pylint: disable=protected-access - renewal._restore_webroot_config(config, renewalparams) - self.assertEqual(config.webroot_path, ["/var/www/"]) - def test_renew_verb_empty_config(self): - rd = os.path.join(self.config_dir, 'renewal') + rd = os.path.join(self.config.config_dir, 'renewal') if not os.path.exists(rd): os.makedirs(rd) with open(os.path.join(rd, 'empty.conf'), 'w'): @@ -845,7 +827,7 @@ class MainTest(test_util.TempDirTestCase): # pylint: disable=too-many-public-me self._test_renewal_common(False, [], args=args, should_renew=False, error_expected=True) def test_renew_with_certname(self): - test_util.make_lineage(self.config_dir, 'sample-renewal.conf') + test_util.make_lineage(self.config.config_dir, 'sample-renewal.conf') self._test_renewal_common(True, [], should_renew=True, args=['renew', '--dry-run', '--cert-name', 'sample-renewal']) @@ -855,7 +837,7 @@ class MainTest(test_util.TempDirTestCase): # pylint: disable=too-many-public-me error_expected=True) def _make_dummy_renewal_config(self): - renewer_configs_dir = os.path.join(self.config_dir, 'renewal') + renewer_configs_dir = os.path.join(self.config.config_dir, 'renewal') os.makedirs(renewer_configs_dir) with open(os.path.join(renewer_configs_dir, 'test.conf'), 'w') as f: f.write("My contents don't matter") @@ -1000,6 +982,7 @@ class MainTest(test_util.TempDirTestCase): # pylint: disable=too-many-public-me mock_get_utility = self._test_certonly_csr_common() cert_msg = mock_get_utility().add_message.call_args_list[0][0][0] self.assertTrue('fullchain.pem' in cert_msg) + self.assertFalse('Your key file has been saved at' in cert_msg) self.assertTrue( 'donate' in mock_get_utility().add_message.call_args[0][0]) @@ -1012,18 +995,24 @@ class MainTest(test_util.TempDirTestCase): # pylint: disable=too-many-public-me @mock.patch('certbot.main.client.acme_client') def test_revoke_with_key(self, mock_acme_client): server = 'foo.bar' - self._call_no_clientmock(['--cert-path', CERT, '--key-path', KEY, + self._call_no_clientmock(['--cert-path', SS_CERT_PATH, '--key-path', RSA2048_KEY_PATH, '--server', server, 'revoke']) - with open(KEY, 'rb') as f: + with open(RSA2048_KEY_PATH, 'rb') as f: mock_acme_client.Client.assert_called_once_with( server, key=jose.JWK.load(f.read()), net=mock.ANY) - with open(CERT, 'rb') as f: + with open(SS_CERT_PATH, 'rb') as f: cert = crypto_util.pyopenssl_load_certificate(f.read())[0] mock_revoke = mock_acme_client.Client().revoke mock_revoke.assert_called_once_with( jose.ComparableX509(cert), mock.ANY) + def test_revoke_with_key_mismatch(self): + server = 'foo.bar' + self.assertRaises(errors.Error, self._call_no_clientmock, + ['--cert-path', CERT, '--key-path', KEY, + '--server', server, 'revoke']) + @mock.patch('certbot.main._determine_account') def test_revoke_without_key(self, mock_determine_account): mock_determine_account.return_value = (mock.MagicMock(), None) diff --git a/certbot/tests/renewal_test.py b/certbot/tests/renewal_test.py index 869e6b104..d292d909a 100644 --- a/certbot/tests/renewal_test.py +++ b/certbot/tests/renewal_test.py @@ -1,5 +1,4 @@ """Tests for certbot.renewal""" -import os import mock import unittest @@ -9,24 +8,22 @@ from certbot import configuration from certbot import errors from certbot import storage -from certbot.tests import util +import certbot.tests.util as test_util -class RenewalTest(util.TempDirTestCase): +class RenewalTest(test_util.ConfigTestCase): def setUp(self): super(RenewalTest, self).setUp() - self.config_dir = os.path.join(self.tempdir, 'config') - @mock.patch('certbot.cli.set_by_cli') def test_ancient_webroot_renewal_conf(self, mock_set_by_cli): mock_set_by_cli.return_value = False - rc_path = util.make_lineage( - self.config_dir, 'sample-renewal-ancient.conf') - args = mock.MagicMock(account=None, config_dir=self.config_dir, - logs_dir="logs", work_dir="work", - email=None, webroot_path=None) - config = configuration.NamespaceConfig(args) + rc_path = test_util.make_lineage( + self.config.config_dir, 'sample-renewal-ancient.conf') + self.config.account = None + self.config.email = None + self.config.webroot_path = None + config = configuration.NamespaceConfig(self.config) lineage = storage.RenewableCert(rc_path, config) renewalparams = lineage.configuration['renewalparams'] # pylint: disable=protected-access @@ -35,10 +32,10 @@ class RenewalTest(util.TempDirTestCase): self.assertEqual(config.webroot_path, ['/var/www/']) -class RestoreRequiredConfigElementsTest(unittest.TestCase): +class RestoreRequiredConfigElementsTest(test_util.ConfigTestCase): """Tests for certbot.renewal.restore_required_config_elements.""" def setUp(self): - self.config = mock.MagicMock() + super(RestoreRequiredConfigElementsTest, self).setUp() @classmethod def _call(cls, *args, **kwargs): diff --git a/certbot/tests/reverter_test.py b/certbot/tests/reverter_test.py index d430f8292..b048737c2 100644 --- a/certbot/tests/reverter_test.py +++ b/certbot/tests/reverter_test.py @@ -14,16 +14,16 @@ from certbot import errors from certbot.tests import util as test_util -class ReverterCheckpointLocalTest(unittest.TestCase): +class ReverterCheckpointLocalTest(test_util.ConfigTestCase): # pylint: disable=too-many-instance-attributes, too-many-public-methods """Test the Reverter Class.""" def setUp(self): + super(ReverterCheckpointLocalTest, self).setUp() from certbot.reverter import Reverter # Disable spurious errors... we are trying to test for them logging.disable(logging.CRITICAL) - self.config = setup_work_direc() self.reverter = Reverter(self.config) tup = setup_test_files() @@ -277,15 +277,15 @@ class ReverterCheckpointLocalTest(unittest.TestCase): self.assertEqual(read_in(self.config2), "directive-dir2") -class TestFullCheckpointsReverter(unittest.TestCase): +class TestFullCheckpointsReverter(test_util.ConfigTestCase): # pylint: disable=too-many-instance-attributes """Tests functions having to deal with full checkpoints.""" def setUp(self): + super(TestFullCheckpointsReverter, self).setUp() from certbot.reverter import Reverter # Disable spurious errors... logging.disable(logging.CRITICAL) - self.config = setup_work_direc() self.reverter = Reverter(self.config) tup = setup_test_files() @@ -439,21 +439,6 @@ class TestFullCheckpointsReverter(unittest.TestCase): return config3 -def setup_work_direc(): - """Setup directories. - - :returns: Mocked :class:`certbot.interfaces.IConfig` - - """ - work_dir = tempfile.mkdtemp("work") - backup_dir = os.path.join(work_dir, "backup") - - return mock.MagicMock( - work_dir=work_dir, backup_dir=backup_dir, - temp_checkpoint_dir=os.path.join(work_dir, "temp"), - in_progress_dir=os.path.join(backup_dir, "in_progress_dir")) - - def setup_test_files(): """Setup sample configuration files.""" dir1 = tempfile.mkdtemp("dir1") diff --git a/certbot/tests/storage_test.py b/certbot/tests/storage_test.py index e6e2b25ff..5e8acc832 100644 --- a/certbot/tests/storage_test.py +++ b/certbot/tests/storage_test.py @@ -13,14 +13,13 @@ import six import certbot from certbot import cli -from certbot import configuration from certbot import errors from certbot.storage import ALL_FOUR -from certbot.tests import util +import certbot.tests.util as test_util -CERT = util.load_cert('cert.pem') +CERT = test_util.load_cert('cert.pem') def unlink_all(rc_object): @@ -36,7 +35,7 @@ def fill_with_sample_data(rc_object): f.write(kind) -class BaseRenewableCertTest(util.TempDirTestCase): +class BaseRenewableCertTest(test_util.ConfigTestCase): """Base class for setting up Renewable Cert tests. .. note:: It may be required to write out self.config for @@ -50,39 +49,31 @@ class BaseRenewableCertTest(util.TempDirTestCase): super(BaseRenewableCertTest, self).setUp() - self.cli_config = configuration.NamespaceConfig( - namespace=mock.MagicMock( - config_dir=self.tempdir, - work_dir=self.tempdir, - logs_dir=self.tempdir, - ) - ) - # TODO: maybe provide NamespaceConfig.make_dirs? # TODO: main() should create those dirs, c.f. #902 - os.makedirs(os.path.join(self.tempdir, "live", "example.org")) - archive_path = os.path.join(self.tempdir, "archive", "example.org") + os.makedirs(os.path.join(self.config.config_dir, "live", "example.org")) + archive_path = os.path.join(self.config.config_dir, "archive", "example.org") os.makedirs(archive_path) - os.makedirs(os.path.join(self.tempdir, "renewal")) + os.makedirs(os.path.join(self.config.config_dir, "renewal")) - config = configobj.ConfigObj() + config_file = configobj.ConfigObj() for kind in ALL_FOUR: - kind_path = os.path.join(self.tempdir, "live", "example.org", + kind_path = os.path.join(self.config.config_dir, "live", "example.org", kind + ".pem") - config[kind] = kind_path - with open(os.path.join(self.tempdir, "live", "example.org", + config_file[kind] = kind_path + with open(os.path.join(self.config.config_dir, "live", "example.org", "README"), 'a'): pass - config["archive"] = archive_path - config.filename = os.path.join(self.tempdir, "renewal", + config_file["archive"] = archive_path + config_file.filename = os.path.join(self.config.config_dir, "renewal", "example.org.conf") - config.write() - self.config = config + config_file.write() + self.config_file = config_file # We also create a file that isn't a renewal config in the same # location to test that logic that reads in all-and-only renewal # configs will ignore it and NOT attempt to parse it. - junk = open(os.path.join(self.tempdir, "renewal", "IGNORE.THIS"), "w") + junk = open(os.path.join(self.config.config_dir, "renewal", "IGNORE.THIS"), "w") junk.write("This file should be ignored!") junk.close() @@ -90,7 +81,7 @@ class BaseRenewableCertTest(util.TempDirTestCase): with mock.patch("certbot.storage.RenewableCert._check_symlinks") as check: check.return_value = True - self.test_rc = storage.RenewableCert(config.filename, self.cli_config) + self.test_rc = storage.RenewableCert(config_file.filename, self.config) def _write_out_kind(self, kind, ver, value=None): link = getattr(self.test_rc, kind) @@ -117,7 +108,7 @@ class RenewableCertTests(BaseRenewableCertTest): for kind in ALL_FOUR: self.assertEqual( getattr(self.test_rc, kind), os.path.join( - self.tempdir, "live", "example.org", kind + ".pem")) + self.config.config_dir, "live", "example.org", kind + ".pem")) def test_renewal_bad_config(self): """Test that the RenewableCert constructor will complain if @@ -125,14 +116,14 @@ class RenewableCertTests(BaseRenewableCertTest): """ from certbot import storage - broken = os.path.join(self.tempdir, "broken.conf") + broken = os.path.join(self.config.config_dir, "broken.conf") with open(broken, "w") as f: f.write("[No closing bracket for you!") self.assertRaises(errors.CertStorageError, storage.RenewableCert, - broken, self.cli_config) + broken, self.config) os.unlink(broken) self.assertRaises(errors.CertStorageError, storage.RenewableCert, - "fun", self.cli_config) + "fun", self.config) def test_renewal_incomplete_config(self): """Test that the RenewableCert constructor will complain if @@ -143,30 +134,30 @@ class RenewableCertTests(BaseRenewableCertTest): # Here the required privkey is missing. config["chain"] = "imaginary_chain.pem" config["fullchain"] = "imaginary_fullchain.pem" - config.filename = os.path.join(self.tempdir, "imaginary_config.conf") + config.filename = os.path.join(self.config.config_dir, "imaginary_config.conf") config.write() self.assertRaises(errors.CertStorageError, storage.RenewableCert, - config.filename, self.cli_config) + config.filename, self.config) def test_no_renewal_version(self): from certbot import storage self._write_out_ex_kinds() - self.assertTrue("version" not in self.config) + self.assertTrue("version" not in self.config_file) with mock.patch("certbot.storage.logger") as mock_logger: - storage.RenewableCert(self.config.filename, self.cli_config) + storage.RenewableCert(self.config_file.filename, self.config) self.assertFalse(mock_logger.warning.called) def test_renewal_newer_version(self): from certbot import storage self._write_out_ex_kinds() - self.config["version"] = "99.99.99" - self.config.write() + self.config_file["version"] = "99.99.99" + self.config_file.write() with mock.patch("certbot.storage.logger") as mock_logger: - storage.RenewableCert(self.config.filename, self.cli_config) + storage.RenewableCert(self.config_file.filename, self.config) self.assertTrue(mock_logger.warning.called) self.assertTrue("version" in mock_logger.warning.call_args[0][0]) @@ -191,7 +182,7 @@ class RenewableCertTests(BaseRenewableCertTest): unlink_all(self.test_rc) # Items must point to desired place if they are absolute for kind in ALL_FOUR: - os.symlink(os.path.join(self.tempdir, kind + "17.pem"), + os.symlink(os.path.join(self.config.config_dir, kind + "17.pem"), getattr(self.test_rc, kind)) self.assertFalse(self.test_rc._consistent()) unlink_all(self.test_rc) @@ -216,17 +207,17 @@ class RenewableCertTests(BaseRenewableCertTest): # Relative path logic self._write_out_kind("cert", 17) self.assertTrue(os.path.samefile(self.test_rc.current_target("cert"), - os.path.join(self.tempdir, "archive", + os.path.join(self.config.config_dir, "archive", "example.org", "cert17.pem"))) # Absolute path logic os.unlink(self.test_rc.cert) - os.symlink(os.path.join(self.tempdir, "archive", "example.org", + os.symlink(os.path.join(self.config.config_dir, "archive", "example.org", "cert17.pem"), self.test_rc.cert) with open(self.test_rc.cert, "w") as f: f.write("cert") self.assertTrue(os.path.samefile(self.test_rc.current_target("cert"), - os.path.join(self.tempdir, "archive", + os.path.join(self.config.config_dir, "archive", "example.org", "cert17.pem"))) @@ -369,18 +360,18 @@ class RenewableCertTests(BaseRenewableCertTest): def test_names(self): # Trying the current version - self._write_out_kind("cert", 12, util.load_vector("cert-san.pem")) + 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 - self._write_out_kind("cert", 15, util.load_vector("cert.pem")) + self._write_out_kind("cert", 15, test_util.load_vector("cert.pem")) self.assertEqual(self.test_rc.names(12), ["example.com", "www.example.com"]) # Testing common name is listed first self._write_out_kind( - "cert", 12, util.load_vector("cert-5sans.pem")) + "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"]) @@ -393,7 +384,7 @@ class RenewableCertTests(BaseRenewableCertTest): def test_time_interval_judgments(self, mock_datetime): """Test should_autodeploy() and should_autorenew() on the basis of expiry time windows.""" - test_cert = util.load_vector("cert.pem") + test_cert = test_util.load_vector("cert.pem") self._write_out_ex_kinds() self.test_rc.update_all_links_to(12) @@ -491,7 +482,7 @@ class RenewableCertTests(BaseRenewableCertTest): self.test_rc.update_all_links_to(3) self.assertEqual( 6, self.test_rc.save_successor(3, b'new cert', None, - b'new chain', self.cli_config)) + b'new chain', self.config)) with open(self.test_rc.version("cert", 6)) as f: self.assertEqual(f.read(), "new cert") with open(self.test_rc.version("chain", 6)) as f: @@ -504,10 +495,10 @@ class RenewableCertTests(BaseRenewableCertTest): # Let's try two more updates self.assertEqual( 7, self.test_rc.save_successor(6, b'again', None, - b'newer chain', self.cli_config)) + b'newer chain', self.config)) self.assertEqual( 8, self.test_rc.save_successor(7, b'hello', None, - b'other chain', self.cli_config)) + b'other chain', self.config)) # All of the subsequent versions should link directly to the original # privkey. for i in (6, 7, 8): @@ -522,14 +513,14 @@ class RenewableCertTests(BaseRenewableCertTest): self.test_rc.update_all_links_to(8) self.assertEqual( 9, self.test_rc.save_successor(8, b'last', None, - b'attempt', self.cli_config)) + b'attempt', self.config)) for kind in ALL_FOUR: self.assertEqual(self.test_rc.available_versions(kind), list(six.moves.range(1, 10))) self.assertEqual(self.test_rc.current_version(kind), 8) with open(self.test_rc.version("fullchain", 9)) as f: self.assertEqual(f.read(), "last" + "attempt") - temp_config_file = os.path.join(self.cli_config.renewal_configs_dir, + temp_config_file = os.path.join(self.config.renewal_configs_dir, self.test_rc.lineagename) + ".conf.new" with open(temp_config_file, "w") as f: f.write("We previously crashed while writing me :(") @@ -537,7 +528,7 @@ class RenewableCertTests(BaseRenewableCertTest): # be saved in a new file rather than creating a new symlink. self.assertEqual( 10, self.test_rc.save_successor(9, b'with', b'a', - b'key', self.cli_config)) + b'key', self.config)) self.assertTrue(os.path.exists(self.test_rc.version("privkey", 10))) self.assertFalse(os.path.islink(self.test_rc.version("privkey", 10))) self.assertFalse(os.path.exists(temp_config_file)) @@ -597,38 +588,38 @@ class RenewableCertTests(BaseRenewableCertTest): from certbot import storage result = storage.RenewableCert.new_lineage( - "the-lineage.com", b"cert", b"privkey", b"chain", self.cli_config) + "the-lineage.com", b"cert", b"privkey", b"chain", self.config) # This consistency check tests most relevant properties about the # newly created cert lineage. # pylint: disable=protected-access self.assertTrue(result._consistent()) self.assertTrue(os.path.exists(os.path.join( - self.cli_config.renewal_configs_dir, "the-lineage.com.conf"))) + self.config.renewal_configs_dir, "the-lineage.com.conf"))) self.assertTrue(os.path.exists(os.path.join( - self.cli_config.live_dir, "the-lineage.com", "README"))) + self.config.live_dir, "the-lineage.com", "README"))) with open(result.fullchain, "rb") as f: self.assertEqual(f.read(), b"cert" + b"chain") # Let's do it again and make sure it makes a different lineage result = storage.RenewableCert.new_lineage( - "the-lineage.com", b"cert2", b"privkey2", b"chain2", self.cli_config) + "the-lineage.com", b"cert2", b"privkey2", b"chain2", self.config) self.assertTrue(os.path.exists(os.path.join( - self.cli_config.renewal_configs_dir, "the-lineage.com-0001.conf"))) + self.config.renewal_configs_dir, "the-lineage.com-0001.conf"))) self.assertTrue(os.path.exists(os.path.join( - self.cli_config.live_dir, "the-lineage.com-0001", "README"))) + self.config.live_dir, "the-lineage.com-0001", "README"))) # Now trigger the detection of already existing files os.mkdir(os.path.join( - self.cli_config.live_dir, "the-lineage.com-0002")) + self.config.live_dir, "the-lineage.com-0002")) self.assertRaises(errors.CertStorageError, storage.RenewableCert.new_lineage, "the-lineage.com", - b"cert3", b"privkey3", b"chain3", self.cli_config) - os.mkdir(os.path.join(self.cli_config.default_archive_dir, "other-example.com")) + b"cert3", b"privkey3", b"chain3", self.config) + os.mkdir(os.path.join(self.config.default_archive_dir, "other-example.com")) self.assertRaises(errors.CertStorageError, storage.RenewableCert.new_lineage, "other-example.com", b"cert4", - b"privkey4", b"chain4", self.cli_config) + b"privkey4", b"chain4", self.config) # Make sure it can accept renewal parameters result = storage.RenewableCert.new_lineage( - "the-lineage.com", b"cert2", b"privkey2", b"chain2", self.cli_config) + "the-lineage.com", b"cert2", b"privkey2", b"chain2", self.config) # TODO: Conceivably we could test that the renewal parameters actually # got saved @@ -640,19 +631,19 @@ class RenewableCertTests(BaseRenewableCertTest): mock_rv.side_effect = lambda x: x from certbot import storage - shutil.rmtree(self.cli_config.renewal_configs_dir) - shutil.rmtree(self.cli_config.default_archive_dir) - shutil.rmtree(self.cli_config.live_dir) + shutil.rmtree(self.config.renewal_configs_dir) + shutil.rmtree(self.config.default_archive_dir) + shutil.rmtree(self.config.live_dir) storage.RenewableCert.new_lineage( - "the-lineage.com", b"cert2", b"privkey2", b"chain2", self.cli_config) + "the-lineage.com", b"cert2", b"privkey2", b"chain2", self.config) self.assertTrue(os.path.exists( os.path.join( - self.cli_config.renewal_configs_dir, "the-lineage.com.conf"))) + self.config.renewal_configs_dir, "the-lineage.com.conf"))) self.assertTrue(os.path.exists(os.path.join( - self.cli_config.live_dir, "the-lineage.com", "privkey.pem"))) + self.config.live_dir, "the-lineage.com", "privkey.pem"))) self.assertTrue(os.path.exists(os.path.join( - self.cli_config.default_archive_dir, "the-lineage.com", "privkey1.pem"))) + self.config.default_archive_dir, "the-lineage.com", "privkey1.pem"))) @mock.patch("certbot.storage.util.unique_lineage_name") def test_invalid_config_filename(self, mock_uln): @@ -660,7 +651,7 @@ class RenewableCertTests(BaseRenewableCertTest): mock_uln.return_value = "this_does_not_end_with_dot_conf", "yikes" self.assertRaises(errors.CertStorageError, storage.RenewableCert.new_lineage, "example.com", - "cert", "privkey", "chain", self.cli_config) + "cert", "privkey", "chain", self.config) def test_bad_kind(self): self.assertRaises( @@ -744,18 +735,18 @@ class RenewableCertTests(BaseRenewableCertTest): from certbot import storage self.assertRaises(errors.CertStorageError, storage.RenewableCert, - self.config.filename, self.cli_config) - os.symlink("missing", self.config[ALL_FOUR[0]]) + self.config_file.filename, self.config) + os.symlink("missing", self.config_file[ALL_FOUR[0]]) self.assertRaises(errors.CertStorageError, storage.RenewableCert, - self.config.filename, self.cli_config) + self.config_file.filename, self.config) def test_write_renewal_config(self): # Mostly tested by the process of creating and updating lineages, # but we can test that this successfully creates files, removes # unneeded items, and preserves comments. - temp = os.path.join(self.tempdir, "sample-file") - temp2 = os.path.join(self.tempdir, "sample-file.new") + temp = os.path.join(self.config.config_dir, "sample-file") + temp2 = os.path.join(self.config.config_dir, "sample-file.new") with open(temp, "w") as f: f.write("[renewalparams]\nuseful = value # A useful value\n" "useless = value # Not needed\n") @@ -785,17 +776,17 @@ class RenewableCertTests(BaseRenewableCertTest): def test_update_symlinks(self): from certbot import storage - archive_dir_path = os.path.join(self.tempdir, "archive", "example.org") + archive_dir_path = os.path.join(self.config.config_dir, "archive", "example.org") for kind in ALL_FOUR: - live_path = self.config[kind] + live_path = self.config_file[kind] basename = kind + "1.pem" archive_path = os.path.join(archive_dir_path, basename) open(archive_path, 'a').close() - os.symlink(os.path.join(self.tempdir, basename), live_path) + os.symlink(os.path.join(self.config.config_dir, basename), live_path) self.assertRaises(errors.CertStorageError, - storage.RenewableCert, self.config.filename, - self.cli_config) - storage.RenewableCert(self.config.filename, self.cli_config, + storage.RenewableCert, self.config_file.filename, + self.config) + storage.RenewableCert(self.config_file.filename, self.config, update_symlinks=True) class DeleteFilesTest(BaseRenewableCertTest): @@ -804,88 +795,88 @@ class DeleteFilesTest(BaseRenewableCertTest): super(DeleteFilesTest, self).setUp() for kind in ALL_FOUR: - kind_path = os.path.join(self.tempdir, "live", "example.org", + kind_path = os.path.join(self.config.config_dir, "live", "example.org", kind + ".pem") with open(kind_path, 'a'): pass - self.config.write() + self.config_file.write() self.assertTrue(os.path.exists(os.path.join( - self.cli_config.renewal_configs_dir, "example.org.conf"))) + self.config.renewal_configs_dir, "example.org.conf"))) self.assertTrue(os.path.exists(os.path.join( - self.cli_config.live_dir, "example.org"))) + self.config.live_dir, "example.org"))) self.assertTrue(os.path.exists(os.path.join( - self.tempdir, "archive", "example.org"))) + self.config.config_dir, "archive", "example.org"))) def _call(self): from certbot import storage with mock.patch("certbot.storage.logger"): - storage.delete_files(self.cli_config, "example.org") + storage.delete_files(self.config, "example.org") def test_delete_all_files(self): self._call() self.assertFalse(os.path.exists(os.path.join( - self.cli_config.renewal_configs_dir, "example.org.conf"))) + self.config.renewal_configs_dir, "example.org.conf"))) self.assertFalse(os.path.exists(os.path.join( - self.cli_config.live_dir, "example.org"))) + self.config.live_dir, "example.org"))) self.assertFalse(os.path.exists(os.path.join( - self.tempdir, "archive", "example.org"))) + self.config.config_dir, "archive", "example.org"))) def test_bad_renewal_config(self): - with open(self.config.filename, 'a') as config_file: + with open(self.config_file.filename, 'a') as config_file: config_file.write("asdfasfasdfasdf") self.assertRaises(errors.CertStorageError, self._call) self.assertTrue(os.path.exists(os.path.join( - self.cli_config.live_dir, "example.org"))) + self.config.live_dir, "example.org"))) self.assertFalse(os.path.exists(os.path.join( - self.cli_config.renewal_configs_dir, "example.org.conf"))) + self.config.renewal_configs_dir, "example.org.conf"))) def test_no_renewal_config(self): - os.remove(self.config.filename) + os.remove(self.config_file.filename) self.assertRaises(errors.CertStorageError, self._call) self.assertTrue(os.path.exists(os.path.join( - self.cli_config.live_dir, "example.org"))) - self.assertFalse(os.path.exists(self.config.filename)) + self.config.live_dir, "example.org"))) + self.assertFalse(os.path.exists(self.config_file.filename)) def test_no_cert_file(self): os.remove(os.path.join( - self.cli_config.live_dir, "example.org", "cert.pem")) + self.config.live_dir, "example.org", "cert.pem")) self._call() - self.assertFalse(os.path.exists(self.config.filename)) + self.assertFalse(os.path.exists(self.config_file.filename)) self.assertFalse(os.path.exists(os.path.join( - self.cli_config.live_dir, "example.org"))) + self.config.live_dir, "example.org"))) self.assertFalse(os.path.exists(os.path.join( - self.tempdir, "archive", "example.org"))) + self.config.config_dir, "archive", "example.org"))) def test_no_readme_file(self): os.remove(os.path.join( - self.cli_config.live_dir, "example.org", "README")) + self.config.live_dir, "example.org", "README")) self._call() - self.assertFalse(os.path.exists(self.config.filename)) + self.assertFalse(os.path.exists(self.config_file.filename)) self.assertFalse(os.path.exists(os.path.join( - self.cli_config.live_dir, "example.org"))) + self.config.live_dir, "example.org"))) self.assertFalse(os.path.exists(os.path.join( - self.tempdir, "archive", "example.org"))) + self.config.config_dir, "archive", "example.org"))) def test_livedir_not_empty(self): with open(os.path.join( - self.cli_config.live_dir, "example.org", "other_file"), 'a'): + self.config.live_dir, "example.org", "other_file"), 'a'): pass self._call() - self.assertFalse(os.path.exists(self.config.filename)) + self.assertFalse(os.path.exists(self.config_file.filename)) self.assertTrue(os.path.exists(os.path.join( - self.cli_config.live_dir, "example.org"))) + self.config.live_dir, "example.org"))) self.assertFalse(os.path.exists(os.path.join( - self.tempdir, "archive", "example.org"))) + self.config.config_dir, "archive", "example.org"))) def test_no_archive(self): - archive_dir = os.path.join(self.tempdir, "archive", "example.org") + archive_dir = os.path.join(self.config.config_dir, "archive", "example.org") os.rmdir(archive_dir) self._call() - self.assertFalse(os.path.exists(self.config.filename)) + self.assertFalse(os.path.exists(self.config_file.filename)) self.assertFalse(os.path.exists(os.path.join( - self.cli_config.live_dir, "example.org"))) + self.config.live_dir, "example.org"))) self.assertFalse(os.path.exists(archive_dir)) diff --git a/certbot/tests/util.py b/certbot/tests/util.py index a36f0f6ac..4ecddc34f 100644 --- a/certbot/tests/util.py +++ b/certbot/tests/util.py @@ -22,6 +22,7 @@ from certbot import constants from certbot import interfaces from certbot import storage from certbot import util +from certbot import configuration from certbot.display import util as display_util @@ -246,6 +247,20 @@ class TempDirTestCase(unittest.TestCase): def tearDown(self): shutil.rmtree(self.tempdir) +class ConfigTestCase(TempDirTestCase): + """Test class which sets up a NamespaceConfig object. + + """ + def setUp(self): + super(ConfigTestCase, self).setUp() + self.config = configuration.NamespaceConfig( + mock.MagicMock( + config_dir=os.path.join(self.tempdir, 'config'), + work_dir=os.path.join(self.tempdir, 'work'), + logs_dir=os.path.join(self.tempdir, 'logs'), + server="example.com", + ) + ) def lock_and_call(func, lock_path): """Grab a lock for lock_path and call func. diff --git a/certbot/util.py b/certbot/util.py index a95ef62b9..98424b496 100644 --- a/certbot/util.py +++ b/certbot/util.py @@ -48,7 +48,7 @@ ANSI_SGR_RESET = "\033[0m" PERM_ERR_FMT = os.linesep.join(( "The following error was encountered:", "{0}", - "If running as non-root, set --config-dir, " + "Either run as root, or set --config-dir, " "--work-dir, and --logs-dir to writeable paths.")) diff --git a/docs/api/cert_manager.rst b/docs/api/cert_manager.rst new file mode 100644 index 000000000..c8c86f8b9 --- /dev/null +++ b/docs/api/cert_manager.rst @@ -0,0 +1,5 @@ +:mod:`certbot.cert_manager` +------------------------------- + +.. automodule:: certbot.cert_manager + :members: diff --git a/docs/api/cli.rst b/docs/api/cli.rst new file mode 100644 index 000000000..91be86758 --- /dev/null +++ b/docs/api/cli.rst @@ -0,0 +1,5 @@ +:mod:`certbot.cli` +---------------------- + +.. automodule:: certbot.cli + :members: diff --git a/docs/api/eff.rst b/docs/api/eff.rst new file mode 100644 index 000000000..2924b256d --- /dev/null +++ b/docs/api/eff.rst @@ -0,0 +1,5 @@ +:mod:`certbot.eff` +---------------------- + +.. automodule:: certbot.eff + :members: diff --git a/docs/api/error_handler.rst b/docs/api/error_handler.rst new file mode 100644 index 000000000..f1306177d --- /dev/null +++ b/docs/api/error_handler.rst @@ -0,0 +1,5 @@ +:mod:`certbot.error_handler` +-------------------------------- + +.. automodule:: certbot.error_handler + :members: diff --git a/docs/api/hooks.rst b/docs/api/hooks.rst new file mode 100644 index 000000000..140fbf284 --- /dev/null +++ b/docs/api/hooks.rst @@ -0,0 +1,5 @@ +:mod:`certbot.hooks` +------------------------ + +.. automodule:: certbot.hooks + :members: diff --git a/docs/api/lock.rst b/docs/api/lock.rst new file mode 100644 index 000000000..6dcbf9589 --- /dev/null +++ b/docs/api/lock.rst @@ -0,0 +1,5 @@ +:mod:`certbot.lock` +----------------------- + +.. automodule:: certbot.lock + :members: diff --git a/docs/api/log.rst b/docs/api/log.rst new file mode 100644 index 000000000..41311de90 --- /dev/null +++ b/docs/api/log.rst @@ -0,0 +1,5 @@ +:mod:`certbot.log` +---------------------- + +.. automodule:: certbot.log + :members: diff --git a/docs/api/main.rst b/docs/api/main.rst new file mode 100644 index 000000000..a555bab01 --- /dev/null +++ b/docs/api/main.rst @@ -0,0 +1,5 @@ +:mod:`certbot.main` +----------------------- + +.. automodule:: certbot.main + :members: diff --git a/docs/api/notify.rst b/docs/api/notify.rst new file mode 100644 index 000000000..fa042b2d2 --- /dev/null +++ b/docs/api/notify.rst @@ -0,0 +1,5 @@ +:mod:`certbot.notify` +------------------------- + +.. automodule:: certbot.notify + :members: diff --git a/docs/api/ocsp.rst b/docs/api/ocsp.rst new file mode 100644 index 000000000..7044f4052 --- /dev/null +++ b/docs/api/ocsp.rst @@ -0,0 +1,5 @@ +:mod:`certbot.ocsp` +----------------------- + +.. automodule:: certbot.ocsp + :members: diff --git a/docs/api/plugins/selection.rst b/docs/api/plugins/selection.rst new file mode 100644 index 000000000..6211bf9c0 --- /dev/null +++ b/docs/api/plugins/selection.rst @@ -0,0 +1,5 @@ +:mod:`certbot.plugins.selection` +------------------------------------ + +.. automodule:: certbot.plugins.selection + :members: diff --git a/docs/api/renewal.rst b/docs/api/renewal.rst new file mode 100644 index 000000000..58557351f --- /dev/null +++ b/docs/api/renewal.rst @@ -0,0 +1,5 @@ +:mod:`certbot.renewal` +-------------------------- + +.. automodule:: certbot.renewal + :members: diff --git a/docs/challenges.rst b/docs/challenges.rst index c00cd3bdf..25d190147 100644 --- a/docs/challenges.rst +++ b/docs/challenges.rst @@ -1,159 +1,85 @@ Challenges ========== -Digital certificates can only be issued to people who are entitled to them. For example, assuming you don't run google.com, you're not entitled to a certificate for it. Nor is someone else entitled to receive a certificate for your web site. - -In order to receive a certificate from Let's Encrypt certificate authority (CA), you have to prove your control over each of the domain names that will be listed in the certificate. You can do so by making certain publicly-visible changes, proving that the person who's requested a particular certificate is the same person who controls the site(s) that the certificate will refer to. - -Let’s Encrypt specifies three different ways to prove your control over a domain (each of which Certbot may be able to do for you). These are called "challenges," because you are being challenged to perform tasks that only someone who controls the domain should be able to accomplish. - -When you use Certbot, it will attempt to help you prove control over your domains automatically in a way that's acceptable to the CA. Especially if this doesn't work the way you expected, it can be helpful to understand what Certbot is trying to do in each case. - -The three ways to prove your control over a domain for the Let’s Encrypt CA are: - -* Posting a specified file on a web site - -This method is called the HTTP-01 challenge. In this challenge, the certificate authority will expect a specified file to be posted in a specified location on a web site. The file will be downloaded using an HTTP request on TCP port 80. Since part of what this challenge shows is the ability to create a file at an arbitrary location, you cannot choose a different location or port number. - -* Offering a specified certificate on a web site - -This method is called the TLS-SNI-01 challenge. In this challenge, the certificate authority will expect a specified digital certificate to be provided by the web server in response to an HTTPS request using a particular made-up domain name. The request will be made using HTTPS on TCP port 443. You cannot choose a different port number. - -This certificate is a self-signed certificate created by Certbot. You use it only temporarily to prove your control over a domain name. It’s not the same as the certificate for your site that will later be issued by Let's Encrypt once you've proven that you control the site. - -* Posting a specified DNS record in the domain name system - -This method is called the DNS-01 challenge. In this challenge, the certificate authority will expect a specified DNS record to be present in your DNS zone when queried for. The record will be a TXT record for a specific subdomain of the name you're proving your control over. - -For each kind of challenge, the challenge can potentially be completed *automatically* (Certbot directly makes the necessary changes itself, or runs another program that does so), or *manually* (Certbot tells you to make a certain change, and you edit a configuration file of some kind in order to accomplish it). Certbot's design emphasizes performing challenges *automatically*, and this is the normal case for most uses of Certbot. - -Some Certbot *plugins* offer the functionality of an *authenticator*, which simply means that they can satisfy challenges. Different plugins can satisfy different kinds of challenges, as follows: - -apache plugin: Can only use TLS-SNI-01. Tries to edit your Apache configuration files in order to temporarily serve a specified Certbot-generated certificate for a specified name. This can work when you're running Certbot on a web server with an existing installation of Apache that is able to listen on port 443. This makes certain assumptions about your Apache configuration. - -nginx plugin: Can only use TLS-SNI-01. Tries to edit your nginx configuration files in order to temporarily serve a specified Certbot-generated certificate for a specified name. This can work when you're running Certbot on a web server with an existing installation of nginx that is able to listen on port 443. This makes certain assumptions about your nginx configuration. - -webroot plugin: Can only use HTTP-01. Tries to place a file into an appropriate place in order for that file to be served over HTTP on port 80 by an existing web server running on your system. This can work when you're running Certbot on a web server with any existing server application that already listens to web requests on port 80, and that serves files from disk in response. - -standalone plugin: Can use either TLS-SNI-01 or HTTP-01. (You can choose with the `--preferred-challenges` option.) Tries to run its own temporary web server which will speak either HTTP on port 80 (for HTTP-01) or HTTPS on port 443 (for TLS-SNI-01). This can work if either of these ports is free to receive incoming connections at the moment that you run Certbot, because there's no existing program listening to them or because you've temporarily shut down any server application that was listening to them. - -manual plugin: Can use either DNS-01 or HTTP-01. May tell you what changes you are expected to make to your configuration. Or, using an external script, can update your DNS records (for DNS-01) or your webroot (for HTTP-01). This can work if you have appropriate technical knowledge of how to make these kinds of changes yourself when asked to do so. Note that this will prevent automated renewal of your certificate using `certbot renew`. [Can manual also use TLS-SNI-01??] - - -Common problems with passing different challenges - -HTTP-01 challenge: -* (With webroot plugin) You aren't running Certbot on your web server - - Most people should install and run Certbot on their web server hosting their website, not on their laptops or some other computer. While you can use Certbot in manual mode on a laptop and then separately set up the appropriate files on your webserver, it's not likely to be the most convenient way to get a certificate for most users. - -* A domain name you're requesting a certificate for isn't correctly pointed at that web server - - In most cases, every name you're requesting a certificate for should already exist and be pointed to the public IP address of the server where you're requesting that certificate. (Some alternatives exist for complex network configurations, but they're the exception rather than the rule.) - -* A firewall is blocking access to port 80 - - The certificate authority needs to be able to connect to port 80 of your server in order to confirm that you satisfied the HTTP-01 challenge. So that needs to be publicly reachable from the Internet, and not blocked by a router or firewall. - -* (With webroot plugin) You specified the webroot directory incorrectly - - If you used `--webroot`, you need to tell Certbot where it can put - files in order to have them served by your existing web server. - If you said your webroot for example.com was /var/www/example.com, - then a file placed in /var/www/example.com/.well-known/acme-challenge/testfile should appear on - your web site at `http://example.com/.well-known/acme-challenge/testfile` (which you can test using a web browser). (A redirection to HTTPS +To receive a certificate from Let's Encrypt certificate authority (CA), you must pass a *challenge* to +prove you control each of the domain names that will be listed in the certificate. A challenge is one of +three tasks that only someone who controls the domain should be able to accomplish: + +* Posting a specified file in a specified location on a web site (the HTTP-01 challenge) +* Offering a specified temporary certificate on a web site (the TLS-SNI-01 challenge) +* Posting a specified DNS record in the domain name system (the DNS-01 challenge) + +It’s possible to complete each type of challenge *automatically* (Certbot directly makes the necessary +changes itself, or runs another program that does so), or *manually* (Certbot tells you to make a +certain change, and you edit a configuration file of some kind in order to accomplish it). Certbot's +design favors performing challenges automatically, and this is the normal case for most users of Certbot. + +Some plugins offer an *authenticator*, meaning that they can satisfy challenges: + +* Apache plugin: (TLS-SNI-01) Tries to edit your Apache configuration files to temporarily serve + a Certbot-generated certificate for a specified name. Use the Apache plugin when you're running + Certbot on a web server with Apache listening on port 443. +* NGINX plugin: (TLS-SNI-01) Tries to edit your NGINX configuration files to temporarily serve a + Certbot-generated certificate for a specified name. Use the NGINX plugin when you're running + Certbot on a web server with NGINX listening on port 443. +* Webroot plugin: (HTTP-01) Tries to place a file where it can be served over HTTP on port 80 by a + web server running on your system. Use the Webroot plugin when you're running Certbot on + a web server with any server application listening on port 80 serving files from a folder on disk in response. +* Standalone plugin: (TLS-SNI-01 or HTTP-01) Tries to run a temporary web server listening on either HTTP on + port 80 (for HTTP-01) or HTTPS on port 443 (for TLS-SNI-01). Use the Standalone plugin if no existing program + is listening to these ports. Choose TLS-SNI-01 or HTTP-01 using the `--preferred-challenges` option. +* Manual plugin: (DNS-01 or HTTP-01) Either tells you what changes to make to your configuration or updates + your DNS records using an external script (for DNS-01) or your webroot (for HTTP-01). Use the Manual + plugin if you have the technical knowledge to make configuration changes yourself when asked to do so. + +Tips for Challenges +------------------- +General tips: + +* Run Certbot on your web server, not on your laptop or another server. It’s usually the easiest way to get a certificate. +* Use a tool like the DNSchecker at dnsstuff.com to check your DNS records to make sure + there are no serious errors. A DNS error can prevent a certificate authority from + issuing a certificate, even if it does not prevent your site from loading in a browser. +* If you are using Apache or NGINX plugins, make sure the configuration of your Apache or NGINX server is correct. + +HTTP-01 Challenge +~~~~~~~~~~~~~~~~~ + +* Make sure the domain name exists and is already pointed to the public IP address of the server where + you’re requesting the certificate. +* Make sure port 80 is open, publicly reachable from the Internet, and not blocked by a router or firewall. +* When using the Webroot plugin or the manual plugin, make sure the the webroot directory exists and that you + specify it properly. If you set the webroot directory for example.com to `/var/www/example.com` + then a file placed in `/var/www/example.com/.well-known/acme-challenge/testfile` should appear on + your web site at `http://example.com/.well-known/acme-challenge/testfile` (A redirection to HTTPS is OK here and should not stop the challenge from working.) +* In some web server configurations, all pages are dynamically generated by some kind of framework, + usually using a database backend. In this case, there might not be a particular directory + from which the web server can serve filesdirectly. Using the Webroot plugin in this case + requires making a change to your web server configuration first. +* Make sure your web server serves files properly from the directory where the challenge + file is placed (e. g. `/.well-known/acme-challenge`) to the expected location on the + website without adding a header or footer. +* When using the Standalone plugin, make sure another program is not already listening to port 80 on the server. +* When using the Webroot plugin, make sure there is a web server listening on port 80. + +TLS-SNI-01 Challenge +~~~~~~~~~~~~~~~~~~~~ + +* The TLS-SNI-01 challenge doesn’t work with content delivery networks (CDNs) + like CloudFlare and Akamai because the domain name is pointed at the CDN, not directly at your server. +* Make sure port 443 is open, publicly reachable from the Internet, and not blocked by a router or firewall. +* When using the Apache plugin, make sure you are running Apache and no other web server on port 443. +* When using the NGINX plugin, make sure you are running NGINX and no other web server on port 443. +* With either the Apache or NGINX plugin, certbot modifies your web server configuration. If you get + an error after successfully completing the challenge, then you have received a certificate but the + plugin was unable to modify your web server configuration, meaning that you'll have to install the certificate manually. + In that case, please file a bug to help us improve certbot! +* When using the Standalone plugin, make sure another program is not already listening to port 443 on the server. + +DNS-01 Challenge +~~~~~~~~~~~~~~~~ + +* When using the manual plugin, make sure your DNS records are correctly updated; + you must be able to make appropriate changes to your DNS zone in order to pass the challenge. - Note that you should *not* specify the .well-known/acme-challenge directory itself. Instead, you should specify the top level directory that web content is served from. - -* (With webroot plugin) You don't have a webroot directory at all - - In some web server configurations, all pages are dynamically generated by some kind of framework, usually using a database backend. In this case, there might not be a particular directory that files can be directly served from by the existing web server application. Using the webroot plugin in this case requires making a change to your web server configuration first. - -* (With manual plugin) You updated the webroot directory incorrectly - - If you used `--manual`, you need to know where you can put files in order to have them served by your existing web server. If you think your webroot for example.com is /var/www/example.com, then a file placed in /var/www/example.com/.well-known/acme-challenge/testfile should appear on - your web site at `http://example.com/.well-known/acme-challenge/testfile`. (A redirection to HTTPS - is OK here and should not stop the challenge from working.) You should also make sure that you don't make a typo in the name of the file when creating it. - -* Your existing web server's configuration refuses to serve files - from /.well-known/acme-challenge, or doesn't serve them at the - /.well-known/acme-challenge location on your site, or serves them - with a header or footer, or serves them with an unusual MIME type. - -* (With standalone plugin) - You tried to use `--standalone` when there was already some other - program on your server listening to port 80 - -* (With webroot plugin) - You tried to use `--webroot` when you don't have an existing web - server listening on port 80 - -* Your DNS records aren't valid - Try checking your DNS records with a tool like the DNSchecker at - http://www.dnsstuff.com/ to make sure there are no serious errors. - Sometimes a DNS error still allows your site to load in a web - browser, but prevents the certificate authority from issuing a - certificate. - -TLS-SNI-01 challenge: -* You aren't running Certbot on your web server - - Most people should install and run Certbot on their web server hosting their website, not on their laptops or some other computer. While you can use Certbot in manual mode on a laptop and then separately set up the appropriate files on your webserver, it's not likely to be the most convenient way to get a certificate for most users. - -* A domain name you're requesting a certificate for isn't correctly - pointed at that web server - - In most cases, every name you're requesting a certificate for should - already exist and be pointed to the server where you're requesting - that certificate. (Some alternatives exist for complex network - configurations, but they're the exception rather than the rule.) - -* You're using a content delivery network (CDN) - - TLS-SNI-01 doesn't work with CDNs (like CloudFlare and Akamai). You - have to use a different challenge type. (This is a special case of - the previous problem: the domain name is pointed at the CDN, not - directly at your server.) - -* A firewall is blocking access to port 443 - - The certificate authority needs to be able to connect to port 443 of - your server in order to confirm that you satisfied the TLS-SNI-01 - challenge. So that needs to be publicly reachable from the Internet, - and not blocked by a router or firewall. - -* (With apache plugin) - Certbot thinks you're running Apache, but you aren't running it, or - you're running a different server of some kind on port 443 - -* (With nginx plugin) - Certbot thinks you're running nginx, but you aren't running it, or - you're running a different server of some kind on port 443 - -* (With apache or nginx plugin) - Certbot doesn't know how to modify your web server configuration correctly - -* (With standalone plugin) - You tried to use `--standalone` when there was already some other - program on your server listening to port 443 - -* Your DNS records aren't valid - Try checking your DNS records with a tool like the DNSchecker at - http://www.dnsstuff.com/ to make sure there are no serious errors. - Sometimes a DNS error still allows your site to load in a web - browser, but prevents the certificate authority from issuing a - certificate. - -DNS-01 challenge: - -* (With manual plugin) Your DNS records weren't correctly updated. - You need to be able to make appropriate changes to your DNS zone - in order to pass the challenge. - -* Your DNS records aren't valid. - Try checking your DNS records with a tool like the DNSchecker at - http://www.dnsstuff.com/ to make sure there are no serious errors. - Sometimes a DNS error still allows your site to load in a web - browser, but prevents the certificate authority from issuing a - certificate. diff --git a/docs/cli-help.txt b/docs/cli-help.txt index 4265056ce..a00fe6c65 100644 --- a/docs/cli-help.txt +++ b/docs/cli-help.txt @@ -39,6 +39,12 @@ optional arguments: -v, --verbose This flag can be used multiple times to incrementally increase the verbosity of output, e.g. -vvv. (default: -2) + --max-log-backups MAX_LOG_BACKUPS + Specifies the maximum number of backup logs that + should be kept by Certbot's built in log rotation. + Setting this flag to 0 disables log rotation entirely, + causing Certbot to always append to the same log file. + (default: 1000) -n, --non-interactive, --noninteractive Run without ever asking for user input. This may require additional command line flags; the client will @@ -68,8 +74,8 @@ optional arguments: 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) + necessary to accurately simulate renewal. --deploy- + hook commands are not called. (default: False) --debug-challenges After setting up challenges, wait for user input before submitting to CA (default: False) --preferred-challenges PREF_CHALLS @@ -89,12 +95,18 @@ optional arguments: case, and to know when to deprecate support for past Python versions and flags. If you wish to hide this information from the Let's Encrypt server, set this to - "". (default: CertbotACMEClient/0.15.0 (certbot; - Ubuntu 16.04.2 LTS) Authenticator/XXX Installer/YYY + "". (default: CertbotACMEClient/0.17.0 (certbot; + Ubuntu 16.04.3 LTS) Authenticator/XXX Installer/YYY (SUBCOMMAND; flags: FLAGS) Py/2.7.12). The flags encoded in the user agent are: --duplicate, --force- renew, --allow-subset-of-names, -n, and whether any hooks are set. + --user-agent-comment USER_AGENT_COMMENT + Add a comment to the default user agent string. May be + used when repackaging Certbot or calling it from + another tool to allow additional statistical data to + be collected. Ignored if --user-agent is set. + (Example: Foo-Wrapper/1.0) (default: None) automation: Arguments for automating execution & other tweaks @@ -274,10 +286,10 @@ renew: attempt was made to obtain/renew a certificate. If multiple renewed certificates have identical post- hooks, only one will be run. (default: None) - --renew-hook RENEW_HOOK + --deploy-hook DEPLOY_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 the + successfully issued certificate. For this command, the + shell variable $RENEWED_LINEAGE will point to the config live subdirectory (for example, "/etc/letsencrypt/live/example.com") containing the new certificates and keys; the shell variable @@ -286,7 +298,7 @@ renew: "example.com www.example.com" (default: None) --disable-hook-validation Ordinarily the commands specified for --pre-hook - /--post-hook/--renew-hook will be checked for + /--post-hook/--deploy-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 @@ -397,15 +409,21 @@ plugins: using DigitalOcean for DNS). (default: False) --dns-dnsimple Obtain certificates using a DNS TXT record (if you are using DNSimple for DNS). (default: False) + --dns-dnsmadeeasy Obtain certificates using a DNS TXT record (if you + areusing DNS Made Easy for DNS). (default: False) --dns-google Obtain certificates using a DNS TXT record (if you are using Google Cloud DNS). (default: False) + --dns-luadns Obtain certificates using a DNS TXT record (if you are + using LuaDNS for DNS). (default: False) --dns-nsone Obtain certificates using a DNS TXT record (if you are using NS1 for DNS). (default: False) + --dns-rfc2136 Obtain certificates using a DNS TXT record (if you are + using BIND for DNS). (default: False) --dns-route53 Obtain certificates using a DNS TXT record (if you are using Route53 for DNS). (default: False) apache: - Apache Web Server plugin + Apache Web Server plugin - Beta --apache-enmod APACHE_ENMOD Path to the Apache 'a2enmod' binary. (default: @@ -487,6 +505,17 @@ dns-dnsimple: --dns-dnsimple-credentials DNS_DNSIMPLE_CREDENTIALS DNSimple credentials INI file. (default: None) +dns-dnsmadeeasy: + Obtain certificates using a DNS TXT record (if you are using DNS Made Easy + for DNS). + + --dns-dnsmadeeasy-propagation-seconds DNS_DNSMADEEASY_PROPAGATION_SECONDS + The number of seconds to wait for DNS to propagate + before asking the ACME server to verify the DNS + record. (default: 60) + --dns-dnsmadeeasy-credentials DNS_DNSMADEEASY_CREDENTIALS + DNS Made Easy credentials INI file. (default: None) + dns-google: Obtain certificates using a DNS TXT record (if you are using Google Cloud DNS for DNS). @@ -504,6 +533,17 @@ dns-google: control#permissions_and_roles for information about therequired permissions.) (default: None) +dns-luadns: + Obtain certificates using a DNS TXT record (if you are using LuaDNS for + DNS). + + --dns-luadns-propagation-seconds DNS_LUADNS_PROPAGATION_SECONDS + The number of seconds to wait for DNS to propagate + before asking the ACME server to verify the DNS + record. (default: 30) + --dns-luadns-credentials DNS_LUADNS_CREDENTIALS + LuaDNS credentials INI file. (default: None) + dns-nsone: Obtain certificates using a DNS TXT record (if you are using NS1 for DNS). @@ -514,6 +554,17 @@ dns-nsone: --dns-nsone-credentials DNS_NSONE_CREDENTIALS NS1 credentials file. (default: None) +dns-rfc2136: + Obtain certificates using a DNS TXT record (if you are using BIND for + DNS). + + --dns-rfc2136-propagation-seconds DNS_RFC2136_PROPAGATION_SECONDS + The number of seconds to wait for DNS to propagate + before asking the ACME server to verify the DNS + record. (default: 60) + --dns-rfc2136-credentials DNS_RFC2136_CREDENTIALS + RFC 2136 credentials INI file. (default: None) + dns-route53: Obtain certificates using a DNS TXT record (if you are using AWS Route53 for DNS). @@ -526,11 +577,16 @@ dns-route53: manual: Authenticate through manual configuration or custom shell scripts. When using shell scripts, an authenticator script must be provided. The - environment variables available to this script are $CERTBOT_DOMAIN which - contains the domain being authenticated, $CERTBOT_VALIDATION which is the - validation string, and $CERTBOT_TOKEN which is the filename of the - resource requested when performing an HTTP-01 challenge. An additional - cleanup script can also be provided and can use the additional variable + environment variables available to this script depend on the type of + challenge. $CERTBOT_DOMAIN will always contain the domain being + authenticated. For HTTP-01 and DNS-01, $CERTBOT_VALIDATION is the + validation string, and $CERTBOT_TOKEN is the filename of the resource + requested when performing an HTTP-01 challenge. When performing a TLS- + SNI-01 challenge, $CERTBOT_SNI_DOMAIN will contain the SNI name for which + the ACME server expects to be presented with the self-signed certificate + located at $CERTBOT_CERT_PATH. The secret key needed to complete the TLS + handshake is located at $CERTBOT_KEY_PATH. An additional cleanup script + can also be provided and can use the additional variable $CERTBOT_AUTH_OUTPUT which contains the stdout output from the auth script. @@ -544,7 +600,7 @@ manual: Automatically allows public IP logging (default: Ask) nginx: - Nginx Web Server plugin + Nginx Web Server plugin - Alpha --nginx-server-root NGINX_SERVER_ROOT Nginx server root directory. (default: /etc/nginx) diff --git a/docs/index.rst b/docs/index.rst index 746080864..17cde1adf 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -5,6 +5,7 @@ Welcome to the Certbot documentation! :maxdepth: 2 intro + what install using contributing diff --git a/docs/install.rst b/docs/install.rst index a1e91c010..fec556ec6 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -89,7 +89,7 @@ You can workaround this restriction by creating a temporary swapfile:: user@webserver:~$ sudo mkswap /tmp/swapfile user@webserver:~$ sudo swapon /tmp/swapfile -Disable and remove the swapfile once the virtual enviroment is constructed:: +Disable and remove the swapfile once the virtual environment is constructed:: user@webserver:~$ sudo swapoff /tmp/swapfile user@webserver:~$ sudo rm /tmp/swapfile @@ -234,11 +234,10 @@ Installing from source Installation from source is only supported for developers and the whole process is described in the :doc:`contributing`. -.. warning:: Please do **not** use ``python setup.py install`` or - ``python pip install .``. Please do **not** attempt the - installation commands as superuser/root and/or without virtual - 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 - Certbot team! +.. warning:: Please do **not** use ``python setup.py install``, ``python pip + install .``, or ``easy_install .``. Please do **not** attempt the + installation commands as superuser/root and/or without virtual 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 Certbot team! diff --git a/docs/intro.rst b/docs/intro.rst index 90c3761ec..2d4abdc2d 100644 --- a/docs/intro.rst +++ b/docs/intro.rst @@ -2,6 +2,9 @@ Introduction ===================== +.. note:: + To get started quickly, use the `interactive installation guide <https://certbot.eff.org>`_. + .. include:: ../README.rst :start-after: tag:intro-begin :end-before: tag:intro-end diff --git a/docs/packaging.rst b/docs/packaging.rst index cc43ee71a..3d58ea92e 100644 --- a/docs/packaging.rst +++ b/docs/packaging.rst @@ -15,8 +15,11 @@ We release packages and upload them to PyPI (wheels and source tarballs). - https://pypi.python.org/pypi/certbot-dns-cloudxns - https://pypi.python.org/pypi/certbot-dns-digitalocean - https://pypi.python.org/pypi/certbot-dns-dnsimple +- https://pypi.python.org/pypi/certbot-dns-dnsmadeeasy - https://pypi.python.org/pypi/certbot-dns-google +- https://pypi.python.org/pypi/certbot-dns-luadns - https://pypi.python.org/pypi/certbot-dns-nsone +- https://pypi.python.org/pypi/certbot-dns-rfc2136 - https://pypi.python.org/pypi/certbot-dns-route53 The following scripts are used in the process: @@ -58,8 +61,11 @@ From our official releases: - https://www.archlinux.org/packages/community/any/certbot-dns-cloudxns - https://www.archlinux.org/packages/community/any/certbot-dns-digitalocean - https://www.archlinux.org/packages/community/any/certbot-dns-dnsimple +- https://www.archlinux.org/packages/community/any/certbot-dns-dnsmadeeasy - https://www.archlinux.org/packages/community/any/certbot-dns-google +- https://www.archlinux.org/packages/community/any/certbot-dns-luadns - https://www.archlinux.org/packages/community/any/certbot-dns-nsone +- https://www.archlinux.org/packages/community/any/certbot-dns-rfc2136 - https://www.archlinux.org/packages/community/any/certbot-dns-route53 From ``master``: https://aur.archlinux.org/packages/certbot-git diff --git a/docs/using.rst b/docs/using.rst index 0b2206db2..8d9a22847 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -39,7 +39,7 @@ serve your website over HTTPS using certificates obtained by certbot. Plugins that do both can be used with the ``certbot run`` command, which is the default when no command is specified. The ``run`` subcommand can also be used to specify -a combination of distinct authenticator and installer plugins. +a combination_ of distinct authenticator and installer plugins. =========== ==== ==== =============================================================== ============================= Plugin Auth Inst Notes Challenge types (and port) @@ -61,7 +61,7 @@ manual_ Y N | Helps you obtain a certificate by giving you instruction =========== ==== ==== =============================================================== ============================= Under the hood, plugins use one of several ACME protocol challenges_ to -prove you control a domain. The options are http-01_ (which uses port 80), +prove you control a domain. The options are http-01_ (which uses port 80), tls-sni-01_ (port 443) and dns-01_ (requiring configuration of a DNS server on port 53, though that's often not the same machine as your webserver). A few plugins support more than one challenge type, in which case you can choose one @@ -102,7 +102,7 @@ If you're getting a certificate for many domains at once, the plugin needs to know where each domain's files are served from, which could potentially be a separate directory for each domain. When requesting a certificate for multiple domains, each domain will use the most recently -specified ``--webroot-path``. So, for instance, +specified ``--webroot-path``. So, for instance, :: @@ -205,6 +205,26 @@ perform the authentication procedure and/or clean up after it by using the ``--manual-auth-hook`` and ``--manual-cleanup-hook`` flags. This is described in more depth in the hooks_ section. +.. _combination: + +Combining plugins +----------------- + +Sometimes you may want to specify a combination of distinct authenticator and +installer plugins. To do so, specify the authenticator plugin with +``--authenticator`` or ``-a`` and the installer plugin with ``--installer`` or +``-i``. + +For instance, you may want to create a certificate using the webroot_ plugin +for authentication and the apache_ plugin for installation, perhaps because you +use a proxy or CDN for SSL and only want to secure the connection between them +and your origin server, which cannot use the tls-sni-01_ challenge due to the +intermediate proxy. + +:: + + certbot run -a webroot -i apache -w /var/www/html -d example.com + .. _third-party-plugins: Third-party plugins @@ -270,6 +290,7 @@ using the ``--cert-name`` flag to specify a particular certificate for the ``run certbot certonly --cert-name example.com +.. _updating_certs: Re-creating and Updating Existing Certificates ---------------------------------------------- @@ -304,7 +325,24 @@ need to issue this command in normal circumstances. ``--expand`` tells Certbot to update an existing certificate with a new certificate that contains all of the old domains and one or more additional -new domains. +new domains. With the ``--expand`` option, use the ``-d`` option to specify +all existing domains and one or more new domains. + +Example: + +.. code-block:: none + + certbot --expand -d existing.com,example.com,newdomain.com + +If you prefer, you can specify the domains individually like this: + +.. code-block:: none + + certbot --expand -d existing.com -d example.com -d newdomain.com + +Consider using ``--cert-name`` instead of ``--expand``, as it gives more control +over which certificate is modified and it lets you remove domains as well as adding them. + ``--allow-subset-of-names`` tells Certbot to continue with certificate generation if only some of the specified domain authorizations can be obtained. This may @@ -318,8 +356,10 @@ certificate counts against several rate limits that are intended to prevent abuse of the ACME protocol, as described `here <https://community.letsencrypt.org/t/rate-limits-for-lets-encrypt/6769>`__. +.. _changing: + Changing a Certificate's Domains --------------------------------- +================================ The ``--cert-name`` flag can also be used to modify the domains a certificate contains, by specifying new domains using the ``-d`` or ``--domains`` flag. If certificate ``example.com`` @@ -343,6 +383,12 @@ use the ``revoke`` command to do so. Note that the ``revoke`` command takes the certbot revoke --cert-path /etc/letsencrypt/live/CERTNAME/cert.pem +You can also specify the reason for revoking your certificate by using the ``reason`` flag. +Reasons include ``unspecified`` which is the default, as well as ``keycompromise``, +``affiliationchanged``, ``superseded``, and ``cessationofoperation``:: + + certbot revoke --cert-path /etc/letsencrypt/live/CERTNAME/cert.pem --reason keycompromise + Additionally, if a certificate is a test certificate obtained via the ``--staging`` or ``--test-cert`` flag, that flag must be passed to the ``revoke`` subcommand. @@ -397,15 +443,15 @@ unnecessarily stopping your webserver. ``--pre-hook`` and ``--post-hook`` hooks run before and after every renewal attempt. If you want your hook to run only after a successful renewal, use -``--renew-hook`` in a command like this. +``--deploy-hook`` in a command like this. -``certbot renew --renew-hook /path/to/renew-hook-script`` +``certbot renew --deploy-hook /path/to/deploy-hook-script`` For example, if you have a daemon that does not read its certificates as the -root user, a renew hook like this can copy them to the correct location and +root user, a deploy hook like this can copy them to the correct location and apply appropriate file permissions. -/path/to/renew-hook-script +/path/to/deploy-hook-script .. code-block:: none @@ -438,7 +484,7 @@ apply appropriate file permissions. esac done -More information about renewal hooks can be found by running +More information about hooks can be found by running ``certbot --help renew``. If you're sure that this command executes successfully without human @@ -504,7 +550,7 @@ renewal configuration file, located at ``/etc/letsencrypt/renewal/CERTNAME``. .. warning:: Modifying any files in ``/etc/letsencrypt`` can damage them so Certbot can no longer properly manage its certificates, and we do not recommend doing so. For most tasks, it is safest to limit yourself to pointing symlinks at the files there, or using -``--renew-hook`` to copy / make new files based upon those files, if your operational situation requires it +``--deploy-hook`` to copy / make new files based upon those files, if your operational situation requires it (for instance, combining certificates and keys in different way, or having copies of things with different specific permissions that are demanded by other programs). @@ -598,7 +644,7 @@ The following files are available: .. note:: All files are PEM-encoded. 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_. + ``--deploy-hook`` if you're using automatic renewal_. .. _hooks: @@ -766,12 +812,24 @@ By default, the following locations are searched: .. keep it up to date with constants.py +.. _log-rotation: + +Log Rotation +============ + +By default certbot stores status logs in ``/var/log/letsencrypt``. By default +certbot will begin rotating logs once there are 1000 logs in the log directory. +Meaning that once 1000 files are in ``/var/log/letsencrypt`` Certbot will delete +the oldest one to make room for new logs. The number of subsequent logs can be +changed by passing the desired number to the command line flag +``--max-log-backups``. + .. _command-line: Certbot command-line options ============================ -Certbot supports a lot of command line options. Here's the full list, from +Certbot supports a lot of command line options. Here's the full list, from ``certbot --help all``: .. literalinclude:: cli-help.txt diff --git a/docs/what.rst b/docs/what.rst new file mode 100644 index 000000000..3d33346c2 --- /dev/null +++ b/docs/what.rst @@ -0,0 +1,31 @@ +====================== +What is a Certificate? +====================== + +A public key or digital *certificate* (formerly called an SSL certificate) uses a public key +and a private key to enable secure communication between a client program (web browser, email client, +etc.) and a server over an encrypted SSL (secure socket layer) or TLS (transport layer security) connection. +The certificate is used both to encrypt the initial stage of communication (secure key exchange) +and to identify the server. The certificate +includes information about the key, information about the server identity, and the digital signature +of the certificate issuer. If the issuer is trusted by the software that initiates the communication, +and the signature is valid, then the key can be used to communicate securely with the server identified by +the certificate. Using a certificate is a good way to prevent "man-in-the-middle" attacks, in which +someone in between you and the server you think you are talking to is able to insert their own (harmful) +content. + +You can use Certbot to easily obtain and configure a free certificate from Let's Encrypt, a +joint project of EFF, Mozilla, and many other sponsors. + +Certificates and Lineages +========================= + +Certbot introduces the concept of a *lineage,* which is a collection of all the versions of a certificate +plus Certbot configuration information maintained for that certificate from +renewal to renewal. Whenever you renew a certificate, Certbot keeps the same configuration unless +you explicitly change it, for example by adding or removing domains. If you add domains, you can +either add them to an existing lineage or create +a new one. + +See also: +:ref:`updating_certs` diff --git a/letsencrypt-auto b/letsencrypt-auto index 725c86895..b935ed447 100755 --- a/letsencrypt-auto +++ b/letsencrypt-auto @@ -28,7 +28,7 @@ if [ -z "$VENV_PATH" ]; then VENV_PATH="$XDG_DATA_HOME/$VENV_NAME" fi VENV_BIN="$VENV_PATH/bin" -LE_AUTO_VERSION="0.15.0" +LE_AUTO_VERSION="0.17.0" BASENAME=$(basename $0) USAGE="Usage: $BASENAME [OPTIONS] A self-updating wrapper script for the Certbot ACME client. When run, updates @@ -200,6 +200,25 @@ ExperimentalBootstrap() { fi } +DeprecationBootstrap() { + # Arguments: Platform name, bootstrap function name + if [ "$DEBUG" = 1 ]; then + if [ "$2" != "" ]; then + BootstrapMessage $1 + $2 + fi + else + error "WARNING: certbot-auto support for this $1 is DEPRECATED!" + error "Please visit certbot.eff.org to learn how to download a version of" + error "Certbot that is packaged for your system. While an existing version" + error "of certbot-auto may work currently, we have stopped supporting updating" + error "system packages for your system. Please switch to a packaged version" + error "as soon as possible." + 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. @@ -630,11 +649,11 @@ Bootstrap() { elif [ -f /etc/manjaro-release ]; then ExperimentalBootstrap "Manjaro Linux" BootstrapArchCommon elif [ -f /etc/gentoo-release ]; then - ExperimentalBootstrap "Gentoo" BootstrapGentooCommon + DeprecationBootstrap "Gentoo" BootstrapGentooCommon elif uname | grep -iq FreeBSD ; then - ExperimentalBootstrap "FreeBSD" BootstrapFreeBsd + DeprecationBootstrap "FreeBSD" BootstrapFreeBsd elif uname | grep -iq Darwin ; then - ExperimentalBootstrap "macOS" BootstrapMac + DeprecationBootstrap "macOS" 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 @@ -710,54 +729,78 @@ pycparser==2.14 \ asn1crypto==0.22.0 \ --hash=sha256:d232509fefcfcdb9a331f37e9c9dc20441019ad927c7d2176cf18ed5da0ba097 \ --hash=sha256:cbbadd640d3165ab24b06ef25d1dca09a3441611ac15f6a6b452474fdf0aed1a -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 +cffi==1.10.0 \ + --hash=sha256:446699c10f3c390633d0722bc19edbc7ac4b94761918a4a4f7908a24e86ebbd0 \ + --hash=sha256:562326fc7f55a59ef3fef5e82908fe938cdc4bbda32d734c424c7cd9ed73e93a \ + --hash=sha256:7f732ad4a30db0b39400c3f7011249f7d0701007d511bf09604729aea222871f \ + --hash=sha256:94fb8410c6c4fc48e7ea759d3d1d9ca561171a88d00faddd4aa0306f698ad6a0 \ + --hash=sha256:587a5043df4b00a2130e09fed42da02a4ed3c688bd9bf07a3ac89d2271f4fb07 \ + --hash=sha256:ec08b88bef627ec1cea210e1608c85d3cf44893bcde74e41b7f7dbdfd2c1bad6 \ + --hash=sha256:a41406f6d62abcdf3eef9fd998d8dcff04fd2a7746644143045feeebd76352d1 \ + --hash=sha256:b560916546b2f209d74b82bdbc3223cee9a165b0242fa00a06dfc48a2054864a \ + --hash=sha256:e74896774e437f4715c57edeb5cf3d3a40d7727f541c2c12156617b5a15d1829 \ + --hash=sha256:9a31c18ba4881a116e448c52f3f5d3e14401cf7a9c43cc88f06f2a7f5428da0e \ + --hash=sha256:80796ea68e11624a0279d3b802f88a7fe7214122b97a15a6c97189934a2cc776 \ + --hash=sha256:f4019826a2dec066c909a1f483ef0dcf9325d6740cc0bd15308942b28b0930f7 \ + --hash=sha256:7248506981eeba23888b4140a69a53c4c0c0a386abcdca61ed8dd790a73e64b9 \ + --hash=sha256:a8955265d146e86fe2ce116394be4eaf0cb40314a79b19f11c4fa574cd639572 \ + --hash=sha256:c49187260043bd4c1d6a52186f9774f17d9b1da0a406798ebf4bfc12da166ade \ + --hash=sha256:c1d8b3d8dcb5c23ac1a8bf56422036f3f305a3c5a8bc8c354256579a1e2aa2c1 \ + --hash=sha256:9e389615bcecb8c782a87939d752340bb0a3a097e90bae54d7f0915bc12f45bd \ + --hash=sha256:d09ff358f75a874f69fa7d1c2b4acecf4282a950293fcfcf89aa606da8a9a500 \ + --hash=sha256:b69b4557aae7de18b7c174a917fe19873529d927ac592762d9771661875bbd40 \ + --hash=sha256:5de52b081a2775e76b971de9d997d85c4457fc0a09079e12d66849548ae60981 \ + --hash=sha256:e7d88fecb7b6250a1fd432e6dc64890342c372fce13dbfe4bb6f16348ad00c14 \ + --hash=sha256:1426e67e855ef7f5030c9184f4f1a9f4bfa020c31c962cd41fd129ec5aef4a6a \ + --hash=sha256:267dd2c66a5760c5f4d47e2ebcf8eeac7ef01e1ae6ae7a6d0d241a290068bc38 \ + --hash=sha256:e553eb489511cacf19eda6e52bc9e151316f0d721724997dda2c4d3079b778db \ + --hash=sha256:98b89b2c57f97ce2db7aeba60db173c84871d73b40e41a11ea95de1500ddc57e \ + --hash=sha256:e2b7e090188833bc58b2ae03fb864c22688654ebd2096bcf38bc860c4f38a3d8 \ + --hash=sha256:afa7d8b8d38ad40db8713ee053d41b36d87d6ae5ec5ad36f9210b548a18dc214 \ + --hash=sha256:4fc9c2ff7924b3a1fa326e1799e5dd58cac585d7fb25fe53ccaa1333b0453d65 \ + --hash=sha256:937db39a1ec5af3003b16357b2042bba67c88d43bc11aaa203fa8a5924524209 \ + --hash=sha256:ab22285797631df3b513b2cd3ecdc51cd8e3d36788e3991d93d0759d6883b027 \ + --hash=sha256:96e599b924ef009aa867f725b3249ee51d76489f484d3a45b4bd219c5ec6ed59 \ + --hash=sha256:bea842a0512be6a8007e585790bccd5d530520fc025ce63b03e139be373b0063 \ + --hash=sha256:e7175287f7fe7b1cc203bb958b17db40abd732690c1e18e700f10e0843a58598 \ + --hash=sha256:285ab352552f52f1398c912556d4d36d4ea9b8450e5c65d03809bf9886755533 \ + --hash=sha256:5576644b859197da7bbd8f8c7c2fb5dcc6cd505cadb42992d5f104c013f8a214 \ + --hash=sha256:b3b02911eb1f6ada203b0763ba924234629b51586f72a21faacc638269f4ced5 ConfigArgParse==0.10.0 \ --hash=sha256:3b50a83dd58149dfcee98cb6565265d10b53e9c0a2bca7eeef7fb5f5524890a7 configobj==5.0.6 \ --hash=sha256:a2f5650770e1c87fb335af19a9b7eb73fc05ccf22144eb68db7d00cd2bcb0902 -cryptography==1.8.2 \ - --hash=sha256:e572527dc4eae300d4ac58c3a49fd0fe1a0178baf341f536d22c45455c958410 \ - --hash=sha256:15448bcfc4ef0f58c8e049f06cb10c296d75456ced02466dff3c82cc9c85f0a6 \ - --hash=sha256:771171a4b7677ee791f74928030fb59ca83a1710d32eaec8395c5170fc520741 \ - --hash=sha256:06e47faef940bc53ca23f5c6a29f5d4ebc47f0c7632f356da8ce4cc3ae99e908 \ - --hash=sha256:730a4f2c028b33b3e6b1a3caa7a3048a1e1e6ff2fe9043acdb21b31a2e711742 \ - --hash=sha256:1bf2299033c5a517014ffd5ec2e267f6a220d9095e75dd002dc33a898a7857cc \ - --hash=sha256:5e7249bc0360d834b89631e83c1a7bbb28098b59fab9816e5e19efdef7b71a1c \ - --hash=sha256:3971bdb5054257c922d95839d10ad347dcaa7137efeed34ce33ee660ea52b7e2 \ - --hash=sha256:a8b431f82972cec974766a484eba02d7bbf6a5c042c13c25f1a23d4a3a31bfb4 \ - --hash=sha256:a9281f0292131747f94219793438d78823bb72fbcafd1b415e99af1d8c42e11c \ - --hash=sha256:502237c0ed9ca77212cf1673627fd2c361ee989cdde2ac54a0cd3f17cbc79e5a \ - --hash=sha256:c63625ec36d1615fff69b068a95ea038d5f8df961901a097dfedc7e7410794d5 \ - --hash=sha256:bda0a32d99ee1f86fcd46bbb10f43216f101df3349187ea8999967cddbfede86 \ - --hash=sha256:a4a69088671eb31aa292a5996d9dd7a4ccb585b6fc7eb7b9e47051e1169fc479 \ - --hash=sha256:0f7127b0034d5112b190de6bf46fadc41940983a91836acfdaa16c44f44beb75 \ - --hash=sha256:9049c073f86155dfcd93434d63db80f753cd2e5bebf1d6172b112de215369b07 \ - --hash=sha256:264dd80150f24a6fffb3ce5be32705e6a27160df055b3582925c2ed170f4f430 \ - --hash=sha256:a05f899c311f6810ae4981edcd23d1587be207de554cf0530f8facbe836483cb \ - --hash=sha256:91970de4b3dbf0b9b36745e9f346265d225d906188dec3d02c2179fbdb49b167 \ - --hash=sha256:908cd59ae3c177c28e7a3eb519dade45748ba9af9959796c699f4f1b56caea8d \ - --hash=sha256:f04fd7c4b7b4c0b97186f31a315fe88d20087a7148ff06a9c0348b35e39531f8 \ - --hash=sha256:757dd412a49ea396b52b051c364bf8f9262dfa6665270f68208a0d6ed5999f1b \ - --hash=sha256:7d6ab4507cf52328b27c57107491b2699a5e25097213a2d201fab0157cb5dd09 \ - --hash=sha256:e3183550d129b7103914cad049a3b2358d9405c6e4baf3a7c92a82004f5e4814 \ - --hash=sha256:9d1f63e2f4530a919ef87b4f1b3d814389604486f8b8c090aefccace4d1f98f8 \ - --hash=sha256:8e88ebac371a388024dab3ccf393bf3c1790d21bc3c299d5a6f9f83fb823beda +cryptography==2.0.2 \ + --hash=sha256:187ae17358436d2c760f28c2aeb02fefa3f37647a9c5b6f7f7c3e83cd1c5a972 \ + --hash=sha256:19e43a13bbf52028dd1e810c803f2ad8880d0692d772f98d42e1eaf34bdee3d6 \ + --hash=sha256:da9291502cbc87dc0284a20c56876e4d2e68deac61cc43df4aec934e44ca97b1 \ + --hash=sha256:0954f8813095f581669330e0a2d5e726c33ac7f450c1458fac58bab54595e516 \ + --hash=sha256:d68b0cc40a8432ed3fc84876c519de704d6001800ec22b136e75ae841910c45b \ + --hash=sha256:2f8ad9580ab4da645cfea52a91d2da99a49a1e76616d8be68441a986fad652b0 \ + --hash=sha256:cc00b4511294f5f6b65c4e77a1a9c62f52490a63d2c120f3872176b40a82351e \ + --hash=sha256:cf896020f6a9f095a547b3d672c8db1ef2ed71fca11250731fa1d4a4cb8b1590 \ + --hash=sha256:e0fdb8322206fa02aa38f71519ff75dce2eb481b7e1110e2936795cb376bb6ee \ + --hash=sha256:277538466657ca5d6637f80be100242f9831d75138b788d718edd3aab34621f8 \ + --hash=sha256:2c77eb0560f54ce654ab82d6b2a64327a71ee969b29022bf9746ca311c9f5069 \ + --hash=sha256:755a7853b679e79d0a799351c092a9b0271f95ff54c8dd8823d8b527a2926a86 \ + --hash=sha256:77197a2d525e761cdd4c771180b4bd0d80703654c6385e4311cbbbe2beb56fa1 \ + --hash=sha256:eb8bb79d0ab00c931c8333b745f06fec481a51c52d70acd4ee95d6093ba5c386 \ + --hash=sha256:131f61de82ef28f3e20beb4bfc24f9692d28cecfd704e20e6c7f070f7793013a \ + --hash=sha256:ac35435974b2e27cd4520f29c191d7da36f4189aa3264e52c4c6c6d089ab6142 \ + --hash=sha256:04b6ea99daa2a8460728794213d76d45ad58ea247dc7e7ff148d7dd726e87863 \ + --hash=sha256:2b9442f8b4c3d575f6cc3db0e856034e0f5a9d55ecd636f52d8c496795b26952 \ + --hash=sha256:b3d3b3ecba1fe1bdb6f180770a137f877c8f07571f7b2934bb269475bcf0e5e8 \ + --hash=sha256:670a58c0d75cb0e78e73dd003bd96d4440bbb1f2bc041dcf7b81767ca4fb0ce9 \ + --hash=sha256:5af84d23bdb86b5e90aca263df1424b43f1748480bfcde3ac2a3cbe622612468 \ + --hash=sha256:ba22e8eefabdd7aca37d0c0c00d2274000d2cebb5cce9e5a710cb55bf8797b31 \ + --hash=sha256:b798b22fa7e92b439547323b8b719d217f1e1b7677585cfeeedf3b55c70bb7fb \ + --hash=sha256:59cff28af8cce96cb7e94a459726e1d88f6f5fa75097f9dcbebd99118d64ea4c \ + --hash=sha256:fe859e445abc9ba9e97950ddafb904e23234c4ecb76b0fae6c86e80592ce464a \ + --hash=sha256:655f3c474067f1e277430f23cc0549f0b1dc99b82aec6e53f80b9b2db7f76f11 \ + --hash=sha256:0ebc2be053c9a03a2f3e20a466e87bf12a51586b3c79bd2a22171b073a805346 \ + --hash=sha256:01e6e60654df64cca53733cda39446d67100c819c181d403afb120e0d2a71e1b \ + --hash=sha256:d46f4e5d455cb5563685c52ef212696f0a6cc1ea627603218eabbd8a095291d8 \ + --hash=sha256:3780b2663ee7ebb37cb83263326e3cd7f8b2ea439c448539d4b87de12c8d06ab enum34==1.1.2 \ --hash=sha256:2475d7fcddf5951e92ff546972758802de5260bf409319a9f1934e6bbc8b1dc7 \ --hash=sha256:35907defb0f992b75ab7788f65fedc1cf20ffa22688e0e6f6f12afc06b3ea501 @@ -864,18 +907,18 @@ letsencrypt==0.7.0 \ --hash=sha256:105a5fb107e45bcd0722eb89696986dcf5f08a86a321d6aef25a0c7c63375ade \ --hash=sha256:c36e532c486a7e92155ee09da54b436a3c420813ec1c590b98f635d924720de9 -certbot==0.15.0 \ - --hash=sha256:f052a1ee9d0e71b73d893c26ee3aa343f6f3abe7de8471437779d541f8bf7824 \ - --hash=sha256:b8c4043b2b8df39660d4ce4a2a6eca590f98ece0e1b97eba53ab95f3bbac3beb -acme==0.15.0 \ - --hash=sha256:d423a14a8fde089d6854ccbe1314f6a80553ef06799ac6f671b90d8399835e60 \ - --hash=sha256:9fadd63322a1eb95f58e6cda8ca2095c750e828ae470bc6e3925ef618c7cfc87 -certbot-apache==0.15.0 \ - --hash=sha256:07fa99b264e0ea489695cc2a5353f3fe9459422ad549de02c46da24ae546acd9 \ - --hash=sha256:6da1433381bd2c2ea7c395be57ca1bcdb7c1c04ce3d12b67a3fa207a3bfa41ca -certbot-nginx==0.15.0 \ - --hash=sha256:7b0622f8a9031e24105f9b5c8d98d4ed83ae393517ed35cc2a4fa50098122922 \ - --hash=sha256:0b98dedb22492d6f88dffdfbd721b4d4c98bfe361df35bc97bf5bd3047f01234 +certbot==0.17.0 \ + --hash=sha256:64c25c7123357feffded6408660bc6f5c7d493dd635ae172081d21473075a86a \ + --hash=sha256:43f5b26c3f314d14babf79a3bdf3522e4fc9eef867a0681c426f113c650a669c +acme==0.17.0 \ + --hash=sha256:501710171633af13fc52aa61d0277a6fe335f7477db5810e72239aaf4f3a09e7 \ + --hash=sha256:3ccbe4aaeb98c77b98ee4093b4e4adb76a1a24cbdfec0130c489c206f1d9b66e +certbot-apache==0.17.0 \ + --hash=sha256:17a7e8d7526d838610e68b96cf052af17c4055655b76b06d1cbc74857d90a216 \ + --hash=sha256:29b9e7bc5eaaff6dc4bce8398e35eeacdf346126aad68cac3d41bb87df20a6b9 +certbot-nginx==0.17.0 \ + --hash=sha256:980c9a33a79ab839a089a0085ff0c5414f01f47b6db26ed342df25916658cec9 \ + --hash=sha256:e573f8b4283172755c07b9cca8a8da7ef2d31b4df763881394b5339b2d42994a UNLIKELY_EOF # ------------------------------------------------------------------------- @@ -903,6 +946,7 @@ anything goes wrong, it will exit with a non-zero status code. # 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 distutils.version import StrictVersion from hashlib import sha256 from os.path import join from pipes import quote @@ -937,12 +981,14 @@ except ImportError: from urllib.parse import urlparse # 3.4 -__version__ = 1, 1, 1 +__version__ = 1, 3, 0 +PIP_VERSION = '9.0.1' # wheel has a conditional dependency on argparse: maybe_argparse = ( - [('https://pypi.python.org/packages/source/a/argparse/' + [('https://pypi.python.org/packages/18/dd/' + 'e617cfc3f6210ae183374cd9f6a26b20514bbb5a792af97949c5aacddf0f/' 'argparse-1.4.0.tar.gz', '62b089a55be1d8949cd2bc7e0df0bddb9e028faefc8c32038cc84862aefdd6e4')] if version_info < (2, 7, 0) else []) @@ -950,13 +996,19 @@ maybe_argparse = ( 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'), + ('https://pypi.python.org/packages/11/b6/' + 'abcb525026a4be042b486df43905d6893fb04f05aac21c32c638e939e447/' + 'pip-{0}.tar.gz' + .format(PIP_VERSION), + '09f243e1a7b461f654c26a725fa373211bb7ff17a9300058b205c61658ca940d'), # This version of setuptools has only optional dependencies: - ('https://pypi.python.org/packages/source/s/setuptools/' + ('https://pypi.python.org/packages/69/65/' + '4c544cde88d4d876cdf5cbc5f3f15d02646477756d89547e9a7ecd6afa76/' 'setuptools-20.2.2.tar.gz', '24fcfc15364a9fe09a220f37d2dcedc849795e3de3e4b393ee988e66a9cbd85a'), - ('https://pypi.python.org/packages/source/w/wheel/wheel-0.29.0.tar.gz', + ('https://pypi.python.org/packages/c9/1d/' + 'bd19e691fd4cfe908c76c429fe6e4436c9e83583c4414b54f6c85471954a/' + 'wheel-0.29.0.tar.gz', '1ebb8ad7e26b448e9caa4773d2357849bf80ff9e313964bcaf79cbf0201a1648') ] @@ -1006,11 +1058,21 @@ def hashed_download(url, temp, digest): def main(): + pip_version = StrictVersion(check_output(['pip', '--version']) + .decode('utf-8').split()[1]) + min_pip_version = StrictVersion(PIP_VERSION) + if pip_version >= min_pip_version: + return 0 + has_pip_cache = pip_version >= StrictVersion('6.0') + 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 ' + + # Disable cache since we're not using it and it otherwise + # sometimes throws permission warnings: + ('--no-cache-dir ' if has_pip_cache else '') + ' '.join(quote(d) for d in downloads), shell=True) except HashError as exc: @@ -1033,9 +1095,9 @@ UNLIKELY_EOF PATH="$VENV_BIN:$PATH" "$VENV_BIN/python" "$TEMP_DIR/pipstrap.py" set +e if [ "$VERBOSE" = 1 ]; then - "$VENV_BIN/pip" install --no-cache-dir --require-hashes -r "$TEMP_DIR/letsencrypt-auto-requirements.txt" + "$VENV_BIN/pip" install --disable-pip-version-check --no-cache-dir --require-hashes -r "$TEMP_DIR/letsencrypt-auto-requirements.txt" else - PIP_OUT=`"$VENV_BIN/pip" install --no-cache-dir --require-hashes -r "$TEMP_DIR/letsencrypt-auto-requirements.txt" 2>&1` + PIP_OUT=`"$VENV_BIN/pip" install --disable-pip-version-check --no-cache-dir --require-hashes -r "$TEMP_DIR/letsencrypt-auto-requirements.txt" 2>&1` fi PIP_STATUS=$? set -e diff --git a/letsencrypt-auto-source/certbot-auto.asc b/letsencrypt-auto-source/certbot-auto.asc index 9c2014502..36afe0eba 100644 --- a/letsencrypt-auto-source/certbot-auto.asc +++ b/letsencrypt-auto-source/certbot-auto.asc @@ -1,11 +1,11 @@ -----BEGIN PGP SIGNATURE----- Version: GnuPG v2 -iQEcBAABCAAGBQJZOXvxAAoJEE0XyZXNl3XyAEEH/3S7/j7XNYv3lfy959ef/vFm -xntReU0J9sOpCLPFHmX2/AFSzxc+L70d082+di50uqnTJHEadvVsnb+XURLAhMeD -GmVjrFkAiigvhX3xWIP3Iu9m/r+wp4h2G6Z6duXcNLhra3Z06n2IxD5rIYG118gH -OnVweYhg6e14B4nbVJBQ2XZp2NfIf9RFE973CQk3YC/cMG4Vfj17n6z8e5nuMZhb -Ztxo9YttWhKfGp7BgbWLZaPJ4XE7LETBYVbBT5V5bSl7ktmftXcOwIUgYcevvZqe -1OLLI8KBLW7OUAuKyivfksn/sIaJY1vvKfj+UruqMCuVGwO8lr4BXKBZxMaq5fk= -=mmbE +iQEcBAABCAAGBQJZgRYdAAoJEE0XyZXNl3XyNskIAMh/M3tV8PTieSrMr3uzLua8 +R+tQJV31WlraoKGQAkZ9Ak+nEhJy0bOi3QAeOmEnS15sBM6ruD+UCfwUDrZxolfW +5Fnue2ocym+MhfDNKoerQNAmaaHY8sutoR+RNTegFyfyr92zMDZVzPm/DFAAHbK+ +eJltSx2Jleaig4V/RcKpkCwHErjQxn6Tn4jHlafAdNL28tEIGXcExpRj4raw3X1L +SoTq/yJiWe+M7t+1iBRVEMZHY1b47PbTo1ipKF/ZZ3Hrz5JKRhAKcA8diHlWp+1I +ujAfU4uu0hR+C3wcpeJ1i2YdS4S9y6uMGyIWU5toJfYdolTSGRZ2lPB+x5Um9pw= +=/7P7 -----END PGP SIGNATURE----- diff --git a/letsencrypt-auto-source/letsencrypt-auto b/letsencrypt-auto-source/letsencrypt-auto index 3e5db44ff..39f8728e8 100755 --- a/letsencrypt-auto-source/letsencrypt-auto +++ b/letsencrypt-auto-source/letsencrypt-auto @@ -28,7 +28,7 @@ if [ -z "$VENV_PATH" ]; then VENV_PATH="$XDG_DATA_HOME/$VENV_NAME" fi VENV_BIN="$VENV_PATH/bin" -LE_AUTO_VERSION="0.16.0.dev0" +LE_AUTO_VERSION="0.18.0.dev0" BASENAME=$(basename $0) USAGE="Usage: $BASENAME [OPTIONS] A self-updating wrapper script for the Certbot ACME client. When run, updates @@ -200,6 +200,25 @@ ExperimentalBootstrap() { fi } +DeprecationBootstrap() { + # Arguments: Platform name, bootstrap function name + if [ "$DEBUG" = 1 ]; then + if [ "$2" != "" ]; then + BootstrapMessage $1 + $2 + fi + else + error "WARNING: certbot-auto support for this $1 is DEPRECATED!" + error "Please visit certbot.eff.org to learn how to download a version of" + error "Certbot that is packaged for your system. While an existing version" + error "of certbot-auto may work currently, we have stopped supporting updating" + error "system packages for your system. Please switch to a packaged version" + error "as soon as possible." + 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. @@ -391,8 +410,7 @@ BootstrapRpmCommon() { ca-certificates " - # Some distros and older versions of current distros use a "python27" - # instead of "python" naming convention. Try both conventions. + # Most RPM distros use the "python" or "python-" naming convention. Let's try that first. if $SUDO $tool list python >/dev/null 2>&1; then pkgs="$pkgs python @@ -401,6 +419,20 @@ BootstrapRpmCommon() { python-tools python-pip " + # Fedora 26 starts to use the prefix python2 for python2 based packages. + # this elseif is theoretically for any Fedora over version 26: + elif $SUDO $tool list python2 >/dev/null 2>&1; then + pkgs="$pkgs + python2 + python2-libs + python2-setuptools + python2-devel + python2-virtualenv + python2-tools + python2-pip + " + # Some distros and older versions of current distros use a "python27" + # instead of the "python" or "python-" naming convention. else pkgs="$pkgs python27 @@ -473,7 +505,7 @@ BootstrapArchCommon() { fi if [ "$missing" ]; then - if [ "$QUIET" = 1]; then + if [ "$QUIET" = 1 ]; then $SUDO pacman -S --needed $missing $noconfirm > /dev/null else $SUDO pacman -S --needed $missing $noconfirm @@ -630,11 +662,11 @@ Bootstrap() { elif [ -f /etc/manjaro-release ]; then ExperimentalBootstrap "Manjaro Linux" BootstrapArchCommon elif [ -f /etc/gentoo-release ]; then - ExperimentalBootstrap "Gentoo" BootstrapGentooCommon + DeprecationBootstrap "Gentoo" BootstrapGentooCommon elif uname | grep -iq FreeBSD ; then - ExperimentalBootstrap "FreeBSD" BootstrapFreeBsd + DeprecationBootstrap "FreeBSD" BootstrapFreeBsd elif uname | grep -iq Darwin ; then - ExperimentalBootstrap "macOS" BootstrapMac + DeprecationBootstrap "macOS" 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 @@ -747,29 +779,41 @@ cffi==1.10.0 \ --hash=sha256:285ab352552f52f1398c912556d4d36d4ea9b8450e5c65d03809bf9886755533 \ --hash=sha256:5576644b859197da7bbd8f8c7c2fb5dcc6cd505cadb42992d5f104c013f8a214 \ --hash=sha256:b3b02911eb1f6ada203b0763ba924234629b51586f72a21faacc638269f4ced5 -ConfigArgParse==0.10.0 \ - --hash=sha256:3b50a83dd58149dfcee98cb6565265d10b53e9c0a2bca7eeef7fb5f5524890a7 +ConfigArgParse==0.12.0 \ + --hash=sha256:28cd7d67669651f2a4518367838c49539457504584a139709b2b8f6c208ef339 configobj==5.0.6 \ --hash=sha256:a2f5650770e1c87fb335af19a9b7eb73fc05ccf22144eb68db7d00cd2bcb0902 -cryptography==1.9 \ - --hash=sha256:469a9d3d851038f1eb7d7f77bb08bb4775b41483372be450e25b293fe57bd59e \ - --hash=sha256:533143321d15c8743f91eec5c5f495c1b5cad9a25de8f6dab01eddd6b416903e \ - --hash=sha256:2230c186182d773064d06242e0fa604cd718bfff28aa9c5ae73d7e426e98a151 \ - --hash=sha256:3dc94ed5a26b8553a325767358f505c0a43e0c89df078647f77a4d022ddcdc57 \ - --hash=sha256:2eb8297b877cb6b56216750fa7017c9f5786bec8afd6a0f1aaace02cbfb6195f \ - --hash=sha256:025a96e48164106f2082b00f42bf430cf21f09e203e42585a712e420b75cbff0 \ - --hash=sha256:61eb3534f8ed2415dd708b28919205d523f220e4cd5b8165844edfdd4a649b8e \ - --hash=sha256:5474fe5ce6b517d3086e0231b6ad88f8978c551c4379f91c3d619c308490f0d7 \ - --hash=sha256:5ff169869624e23767d70db274f13a9ea4e97c029425a1224aa5e049e84ce2af \ - --hash=sha256:c26e057a2de13e97e708328d295c5ac4cd3eab4a5c42ce727dd1a53316034b8a \ - --hash=sha256:7db719432648f14ea33edffc5f75330c595804eac86ca916528b35ce50a8bfd6 \ - --hash=sha256:ca72537a7064bb80e34b6908e576ffc8e2c2cad29a7168f48d0999df089695c3 \ - --hash=sha256:2a5e577f5d2093e51486b4ec02b51bb5adb165b98fee99929d5af0813e90b469 \ - --hash=sha256:9d9da8bac2e31003d092f5ef6981a725c77c4e9a30638436884d61ad39f9a1ee \ - --hash=sha256:fab8ec6866e384d9827d5dc02a1383e991a6c05c54f818316c4b829e56ca2ba3 \ - --hash=sha256:365eb804362e581c9a02e7a610b30514f07dd77b2a38aed338eb5192446bbc58 \ - --hash=sha256:2908f709f02711dbb10561a9f154d2f7d1792385e2341e9708539cc4ecfb8667 \ - --hash=sha256:5518337022718029e367d982642f3e3523541e098ad671672a90b82474c84882 +cryptography==2.0.2 \ + --hash=sha256:187ae17358436d2c760f28c2aeb02fefa3f37647a9c5b6f7f7c3e83cd1c5a972 \ + --hash=sha256:19e43a13bbf52028dd1e810c803f2ad8880d0692d772f98d42e1eaf34bdee3d6 \ + --hash=sha256:da9291502cbc87dc0284a20c56876e4d2e68deac61cc43df4aec934e44ca97b1 \ + --hash=sha256:0954f8813095f581669330e0a2d5e726c33ac7f450c1458fac58bab54595e516 \ + --hash=sha256:d68b0cc40a8432ed3fc84876c519de704d6001800ec22b136e75ae841910c45b \ + --hash=sha256:2f8ad9580ab4da645cfea52a91d2da99a49a1e76616d8be68441a986fad652b0 \ + --hash=sha256:cc00b4511294f5f6b65c4e77a1a9c62f52490a63d2c120f3872176b40a82351e \ + --hash=sha256:cf896020f6a9f095a547b3d672c8db1ef2ed71fca11250731fa1d4a4cb8b1590 \ + --hash=sha256:e0fdb8322206fa02aa38f71519ff75dce2eb481b7e1110e2936795cb376bb6ee \ + --hash=sha256:277538466657ca5d6637f80be100242f9831d75138b788d718edd3aab34621f8 \ + --hash=sha256:2c77eb0560f54ce654ab82d6b2a64327a71ee969b29022bf9746ca311c9f5069 \ + --hash=sha256:755a7853b679e79d0a799351c092a9b0271f95ff54c8dd8823d8b527a2926a86 \ + --hash=sha256:77197a2d525e761cdd4c771180b4bd0d80703654c6385e4311cbbbe2beb56fa1 \ + --hash=sha256:eb8bb79d0ab00c931c8333b745f06fec481a51c52d70acd4ee95d6093ba5c386 \ + --hash=sha256:131f61de82ef28f3e20beb4bfc24f9692d28cecfd704e20e6c7f070f7793013a \ + --hash=sha256:ac35435974b2e27cd4520f29c191d7da36f4189aa3264e52c4c6c6d089ab6142 \ + --hash=sha256:04b6ea99daa2a8460728794213d76d45ad58ea247dc7e7ff148d7dd726e87863 \ + --hash=sha256:2b9442f8b4c3d575f6cc3db0e856034e0f5a9d55ecd636f52d8c496795b26952 \ + --hash=sha256:b3d3b3ecba1fe1bdb6f180770a137f877c8f07571f7b2934bb269475bcf0e5e8 \ + --hash=sha256:670a58c0d75cb0e78e73dd003bd96d4440bbb1f2bc041dcf7b81767ca4fb0ce9 \ + --hash=sha256:5af84d23bdb86b5e90aca263df1424b43f1748480bfcde3ac2a3cbe622612468 \ + --hash=sha256:ba22e8eefabdd7aca37d0c0c00d2274000d2cebb5cce9e5a710cb55bf8797b31 \ + --hash=sha256:b798b22fa7e92b439547323b8b719d217f1e1b7677585cfeeedf3b55c70bb7fb \ + --hash=sha256:59cff28af8cce96cb7e94a459726e1d88f6f5fa75097f9dcbebd99118d64ea4c \ + --hash=sha256:fe859e445abc9ba9e97950ddafb904e23234c4ecb76b0fae6c86e80592ce464a \ + --hash=sha256:655f3c474067f1e277430f23cc0549f0b1dc99b82aec6e53f80b9b2db7f76f11 \ + --hash=sha256:0ebc2be053c9a03a2f3e20a466e87bf12a51586b3c79bd2a22171b073a805346 \ + --hash=sha256:01e6e60654df64cca53733cda39446d67100c819c181d403afb120e0d2a71e1b \ + --hash=sha256:d46f4e5d455cb5563685c52ef212696f0a6cc1ea627603218eabbd8a095291d8 \ + --hash=sha256:3780b2663ee7ebb37cb83263326e3cd7f8b2ea439c448539d4b87de12c8d06ab enum34==1.1.2 \ --hash=sha256:2475d7fcddf5951e92ff546972758802de5260bf409319a9f1934e6bbc8b1dc7 \ --hash=sha256:35907defb0f992b75ab7788f65fedc1cf20ffa22688e0e6f6f12afc06b3ea501 @@ -876,18 +920,18 @@ letsencrypt==0.7.0 \ --hash=sha256:105a5fb107e45bcd0722eb89696986dcf5f08a86a321d6aef25a0c7c63375ade \ --hash=sha256:c36e532c486a7e92155ee09da54b436a3c420813ec1c590b98f635d924720de9 -certbot==0.15.0 \ - --hash=sha256:f052a1ee9d0e71b73d893c26ee3aa343f6f3abe7de8471437779d541f8bf7824 \ - --hash=sha256:b8c4043b2b8df39660d4ce4a2a6eca590f98ece0e1b97eba53ab95f3bbac3beb -acme==0.15.0 \ - --hash=sha256:d423a14a8fde089d6854ccbe1314f6a80553ef06799ac6f671b90d8399835e60 \ - --hash=sha256:9fadd63322a1eb95f58e6cda8ca2095c750e828ae470bc6e3925ef618c7cfc87 -certbot-apache==0.15.0 \ - --hash=sha256:07fa99b264e0ea489695cc2a5353f3fe9459422ad549de02c46da24ae546acd9 \ - --hash=sha256:6da1433381bd2c2ea7c395be57ca1bcdb7c1c04ce3d12b67a3fa207a3bfa41ca -certbot-nginx==0.15.0 \ - --hash=sha256:7b0622f8a9031e24105f9b5c8d98d4ed83ae393517ed35cc2a4fa50098122922 \ - --hash=sha256:0b98dedb22492d6f88dffdfbd721b4d4c98bfe361df35bc97bf5bd3047f01234 +certbot==0.17.0 \ + --hash=sha256:64c25c7123357feffded6408660bc6f5c7d493dd635ae172081d21473075a86a \ + --hash=sha256:43f5b26c3f314d14babf79a3bdf3522e4fc9eef867a0681c426f113c650a669c +acme==0.17.0 \ + --hash=sha256:501710171633af13fc52aa61d0277a6fe335f7477db5810e72239aaf4f3a09e7 \ + --hash=sha256:3ccbe4aaeb98c77b98ee4093b4e4adb76a1a24cbdfec0130c489c206f1d9b66e +certbot-apache==0.17.0 \ + --hash=sha256:17a7e8d7526d838610e68b96cf052af17c4055655b76b06d1cbc74857d90a216 \ + --hash=sha256:29b9e7bc5eaaff6dc4bce8398e35eeacdf346126aad68cac3d41bb87df20a6b9 +certbot-nginx==0.17.0 \ + --hash=sha256:980c9a33a79ab839a089a0085ff0c5414f01f47b6db26ed342df25916658cec9 \ + --hash=sha256:e573f8b4283172755c07b9cca8a8da7ef2d31b4df763881394b5339b2d42994a UNLIKELY_EOF # ------------------------------------------------------------------------- @@ -915,6 +959,7 @@ anything goes wrong, it will exit with a non-zero status code. # 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 distutils.version import StrictVersion from hashlib import sha256 from os.path import join from pipes import quote @@ -949,12 +994,14 @@ except ImportError: from urllib.parse import urlparse # 3.4 -__version__ = 1, 1, 1 +__version__ = 1, 3, 0 +PIP_VERSION = '9.0.1' # wheel has a conditional dependency on argparse: maybe_argparse = ( - [('https://pypi.python.org/packages/source/a/argparse/' + [('https://pypi.python.org/packages/18/dd/' + 'e617cfc3f6210ae183374cd9f6a26b20514bbb5a792af97949c5aacddf0f/' 'argparse-1.4.0.tar.gz', '62b089a55be1d8949cd2bc7e0df0bddb9e028faefc8c32038cc84862aefdd6e4')] if version_info < (2, 7, 0) else []) @@ -962,13 +1009,19 @@ maybe_argparse = ( 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'), + ('https://pypi.python.org/packages/11/b6/' + 'abcb525026a4be042b486df43905d6893fb04f05aac21c32c638e939e447/' + 'pip-{0}.tar.gz' + .format(PIP_VERSION), + '09f243e1a7b461f654c26a725fa373211bb7ff17a9300058b205c61658ca940d'), # This version of setuptools has only optional dependencies: - ('https://pypi.python.org/packages/source/s/setuptools/' + ('https://pypi.python.org/packages/69/65/' + '4c544cde88d4d876cdf5cbc5f3f15d02646477756d89547e9a7ecd6afa76/' 'setuptools-20.2.2.tar.gz', '24fcfc15364a9fe09a220f37d2dcedc849795e3de3e4b393ee988e66a9cbd85a'), - ('https://pypi.python.org/packages/source/w/wheel/wheel-0.29.0.tar.gz', + ('https://pypi.python.org/packages/c9/1d/' + 'bd19e691fd4cfe908c76c429fe6e4436c9e83583c4414b54f6c85471954a/' + 'wheel-0.29.0.tar.gz', '1ebb8ad7e26b448e9caa4773d2357849bf80ff9e313964bcaf79cbf0201a1648') ] @@ -1018,11 +1071,21 @@ def hashed_download(url, temp, digest): def main(): + pip_version = StrictVersion(check_output(['pip', '--version']) + .decode('utf-8').split()[1]) + min_pip_version = StrictVersion(PIP_VERSION) + if pip_version >= min_pip_version: + return 0 + has_pip_cache = pip_version >= StrictVersion('6.0') + 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 ' + + # Disable cache since we're not using it and it otherwise + # sometimes throws permission warnings: + ('--no-cache-dir ' if has_pip_cache else '') + ' '.join(quote(d) for d in downloads), shell=True) except HashError as exc: @@ -1045,9 +1108,9 @@ UNLIKELY_EOF PATH="$VENV_BIN:$PATH" "$VENV_BIN/python" "$TEMP_DIR/pipstrap.py" set +e if [ "$VERBOSE" = 1 ]; then - "$VENV_BIN/pip" install --no-cache-dir --require-hashes -r "$TEMP_DIR/letsencrypt-auto-requirements.txt" + "$VENV_BIN/pip" install --disable-pip-version-check --no-cache-dir --require-hashes -r "$TEMP_DIR/letsencrypt-auto-requirements.txt" else - PIP_OUT=`"$VENV_BIN/pip" install --no-cache-dir --require-hashes -r "$TEMP_DIR/letsencrypt-auto-requirements.txt" 2>&1` + PIP_OUT=`"$VENV_BIN/pip" install --disable-pip-version-check --no-cache-dir --require-hashes -r "$TEMP_DIR/letsencrypt-auto-requirements.txt" 2>&1` fi PIP_STATUS=$? set -e diff --git a/letsencrypt-auto-source/letsencrypt-auto.sig b/letsencrypt-auto-source/letsencrypt-auto.sig Binary files differindex 96ad67971..a8885d19a 100644 --- a/letsencrypt-auto-source/letsencrypt-auto.sig +++ b/letsencrypt-auto-source/letsencrypt-auto.sig diff --git a/letsencrypt-auto-source/letsencrypt-auto.template b/letsencrypt-auto-source/letsencrypt-auto.template index 305435a9b..284241a82 100755 --- a/letsencrypt-auto-source/letsencrypt-auto.template +++ b/letsencrypt-auto-source/letsencrypt-auto.template @@ -200,6 +200,25 @@ ExperimentalBootstrap() { fi } +DeprecationBootstrap() { + # Arguments: Platform name, bootstrap function name + if [ "$DEBUG" = 1 ]; then + if [ "$2" != "" ]; then + BootstrapMessage $1 + $2 + fi + else + error "WARNING: certbot-auto support for this $1 is DEPRECATED!" + error "Please visit certbot.eff.org to learn how to download a version of" + error "Certbot that is packaged for your system. While an existing version" + error "of certbot-auto may work currently, we have stopped supporting updating" + error "system packages for your system. Please switch to a packaged version" + error "as soon as possible." + 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. @@ -260,11 +279,11 @@ Bootstrap() { elif [ -f /etc/manjaro-release ]; then ExperimentalBootstrap "Manjaro Linux" BootstrapArchCommon elif [ -f /etc/gentoo-release ]; then - ExperimentalBootstrap "Gentoo" BootstrapGentooCommon + DeprecationBootstrap "Gentoo" BootstrapGentooCommon elif uname | grep -iq FreeBSD ; then - ExperimentalBootstrap "FreeBSD" BootstrapFreeBsd + DeprecationBootstrap "FreeBSD" BootstrapFreeBsd elif uname | grep -iq Darwin ; then - ExperimentalBootstrap "macOS" BootstrapMac + DeprecationBootstrap "macOS" 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 @@ -330,9 +349,9 @@ UNLIKELY_EOF PATH="$VENV_BIN:$PATH" "$VENV_BIN/python" "$TEMP_DIR/pipstrap.py" set +e if [ "$VERBOSE" = 1 ]; then - "$VENV_BIN/pip" install --no-cache-dir --require-hashes -r "$TEMP_DIR/letsencrypt-auto-requirements.txt" + "$VENV_BIN/pip" install --disable-pip-version-check --no-cache-dir --require-hashes -r "$TEMP_DIR/letsencrypt-auto-requirements.txt" else - PIP_OUT=`"$VENV_BIN/pip" install --no-cache-dir --require-hashes -r "$TEMP_DIR/letsencrypt-auto-requirements.txt" 2>&1` + PIP_OUT=`"$VENV_BIN/pip" install --disable-pip-version-check --no-cache-dir --require-hashes -r "$TEMP_DIR/letsencrypt-auto-requirements.txt" 2>&1` fi PIP_STATUS=$? set -e diff --git a/letsencrypt-auto-source/pieces/bootstrappers/arch_common.sh b/letsencrypt-auto-source/pieces/bootstrappers/arch_common.sh index c3959484b..e9d91fe70 100755 --- a/letsencrypt-auto-source/pieces/bootstrappers/arch_common.sh +++ b/letsencrypt-auto-source/pieces/bootstrappers/arch_common.sh @@ -25,7 +25,7 @@ BootstrapArchCommon() { fi if [ "$missing" ]; then - if [ "$QUIET" = 1]; then + if [ "$QUIET" = 1 ]; then $SUDO pacman -S --needed $missing $noconfirm > /dev/null else $SUDO pacman -S --needed $missing $noconfirm diff --git a/letsencrypt-auto-source/pieces/bootstrappers/rpm_common.sh b/letsencrypt-auto-source/pieces/bootstrappers/rpm_common.sh index dcd535292..965ee32f3 100755 --- a/letsencrypt-auto-source/pieces/bootstrappers/rpm_common.sh +++ b/letsencrypt-auto-source/pieces/bootstrappers/rpm_common.sh @@ -54,8 +54,7 @@ BootstrapRpmCommon() { ca-certificates " - # Some distros and older versions of current distros use a "python27" - # instead of "python" naming convention. Try both conventions. + # Most RPM distros use the "python" or "python-" naming convention. Let's try that first. if $SUDO $tool list python >/dev/null 2>&1; then pkgs="$pkgs python @@ -64,6 +63,20 @@ BootstrapRpmCommon() { python-tools python-pip " + # Fedora 26 starts to use the prefix python2 for python2 based packages. + # this elseif is theoretically for any Fedora over version 26: + elif $SUDO $tool list python2 >/dev/null 2>&1; then + pkgs="$pkgs + python2 + python2-libs + python2-setuptools + python2-devel + python2-virtualenv + python2-tools + python2-pip + " + # Some distros and older versions of current distros use a "python27" + # instead of the "python" or "python-" naming convention. else pkgs="$pkgs python27 diff --git a/letsencrypt-auto-source/pieces/certbot-requirements.txt b/letsencrypt-auto-source/pieces/certbot-requirements.txt index da9c33583..808a6a8a5 100644 --- a/letsencrypt-auto-source/pieces/certbot-requirements.txt +++ b/letsencrypt-auto-source/pieces/certbot-requirements.txt @@ -1,12 +1,12 @@ -certbot==0.15.0 \ - --hash=sha256:f052a1ee9d0e71b73d893c26ee3aa343f6f3abe7de8471437779d541f8bf7824 \ - --hash=sha256:b8c4043b2b8df39660d4ce4a2a6eca590f98ece0e1b97eba53ab95f3bbac3beb -acme==0.15.0 \ - --hash=sha256:d423a14a8fde089d6854ccbe1314f6a80553ef06799ac6f671b90d8399835e60 \ - --hash=sha256:9fadd63322a1eb95f58e6cda8ca2095c750e828ae470bc6e3925ef618c7cfc87 -certbot-apache==0.15.0 \ - --hash=sha256:07fa99b264e0ea489695cc2a5353f3fe9459422ad549de02c46da24ae546acd9 \ - --hash=sha256:6da1433381bd2c2ea7c395be57ca1bcdb7c1c04ce3d12b67a3fa207a3bfa41ca -certbot-nginx==0.15.0 \ - --hash=sha256:7b0622f8a9031e24105f9b5c8d98d4ed83ae393517ed35cc2a4fa50098122922 \ - --hash=sha256:0b98dedb22492d6f88dffdfbd721b4d4c98bfe361df35bc97bf5bd3047f01234 +certbot==0.17.0 \ + --hash=sha256:64c25c7123357feffded6408660bc6f5c7d493dd635ae172081d21473075a86a \ + --hash=sha256:43f5b26c3f314d14babf79a3bdf3522e4fc9eef867a0681c426f113c650a669c +acme==0.17.0 \ + --hash=sha256:501710171633af13fc52aa61d0277a6fe335f7477db5810e72239aaf4f3a09e7 \ + --hash=sha256:3ccbe4aaeb98c77b98ee4093b4e4adb76a1a24cbdfec0130c489c206f1d9b66e +certbot-apache==0.17.0 \ + --hash=sha256:17a7e8d7526d838610e68b96cf052af17c4055655b76b06d1cbc74857d90a216 \ + --hash=sha256:29b9e7bc5eaaff6dc4bce8398e35eeacdf346126aad68cac3d41bb87df20a6b9 +certbot-nginx==0.17.0 \ + --hash=sha256:980c9a33a79ab839a089a0085ff0c5414f01f47b6db26ed342df25916658cec9 \ + --hash=sha256:e573f8b4283172755c07b9cca8a8da7ef2d31b4df763881394b5339b2d42994a diff --git a/letsencrypt-auto-source/pieces/dependency-requirements.txt b/letsencrypt-auto-source/pieces/dependency-requirements.txt index a8007ba3e..4b3da685c 100644 --- a/letsencrypt-auto-source/pieces/dependency-requirements.txt +++ b/letsencrypt-auto-source/pieces/dependency-requirements.txt @@ -58,29 +58,41 @@ cffi==1.10.0 \ --hash=sha256:285ab352552f52f1398c912556d4d36d4ea9b8450e5c65d03809bf9886755533 \ --hash=sha256:5576644b859197da7bbd8f8c7c2fb5dcc6cd505cadb42992d5f104c013f8a214 \ --hash=sha256:b3b02911eb1f6ada203b0763ba924234629b51586f72a21faacc638269f4ced5 -ConfigArgParse==0.10.0 \ - --hash=sha256:3b50a83dd58149dfcee98cb6565265d10b53e9c0a2bca7eeef7fb5f5524890a7 +ConfigArgParse==0.12.0 \ + --hash=sha256:28cd7d67669651f2a4518367838c49539457504584a139709b2b8f6c208ef339 configobj==5.0.6 \ --hash=sha256:a2f5650770e1c87fb335af19a9b7eb73fc05ccf22144eb68db7d00cd2bcb0902 -cryptography==1.9 \ - --hash=sha256:469a9d3d851038f1eb7d7f77bb08bb4775b41483372be450e25b293fe57bd59e \ - --hash=sha256:533143321d15c8743f91eec5c5f495c1b5cad9a25de8f6dab01eddd6b416903e \ - --hash=sha256:2230c186182d773064d06242e0fa604cd718bfff28aa9c5ae73d7e426e98a151 \ - --hash=sha256:3dc94ed5a26b8553a325767358f505c0a43e0c89df078647f77a4d022ddcdc57 \ - --hash=sha256:2eb8297b877cb6b56216750fa7017c9f5786bec8afd6a0f1aaace02cbfb6195f \ - --hash=sha256:025a96e48164106f2082b00f42bf430cf21f09e203e42585a712e420b75cbff0 \ - --hash=sha256:61eb3534f8ed2415dd708b28919205d523f220e4cd5b8165844edfdd4a649b8e \ - --hash=sha256:5474fe5ce6b517d3086e0231b6ad88f8978c551c4379f91c3d619c308490f0d7 \ - --hash=sha256:5ff169869624e23767d70db274f13a9ea4e97c029425a1224aa5e049e84ce2af \ - --hash=sha256:c26e057a2de13e97e708328d295c5ac4cd3eab4a5c42ce727dd1a53316034b8a \ - --hash=sha256:7db719432648f14ea33edffc5f75330c595804eac86ca916528b35ce50a8bfd6 \ - --hash=sha256:ca72537a7064bb80e34b6908e576ffc8e2c2cad29a7168f48d0999df089695c3 \ - --hash=sha256:2a5e577f5d2093e51486b4ec02b51bb5adb165b98fee99929d5af0813e90b469 \ - --hash=sha256:9d9da8bac2e31003d092f5ef6981a725c77c4e9a30638436884d61ad39f9a1ee \ - --hash=sha256:fab8ec6866e384d9827d5dc02a1383e991a6c05c54f818316c4b829e56ca2ba3 \ - --hash=sha256:365eb804362e581c9a02e7a610b30514f07dd77b2a38aed338eb5192446bbc58 \ - --hash=sha256:2908f709f02711dbb10561a9f154d2f7d1792385e2341e9708539cc4ecfb8667 \ - --hash=sha256:5518337022718029e367d982642f3e3523541e098ad671672a90b82474c84882 +cryptography==2.0.2 \ + --hash=sha256:187ae17358436d2c760f28c2aeb02fefa3f37647a9c5b6f7f7c3e83cd1c5a972 \ + --hash=sha256:19e43a13bbf52028dd1e810c803f2ad8880d0692d772f98d42e1eaf34bdee3d6 \ + --hash=sha256:da9291502cbc87dc0284a20c56876e4d2e68deac61cc43df4aec934e44ca97b1 \ + --hash=sha256:0954f8813095f581669330e0a2d5e726c33ac7f450c1458fac58bab54595e516 \ + --hash=sha256:d68b0cc40a8432ed3fc84876c519de704d6001800ec22b136e75ae841910c45b \ + --hash=sha256:2f8ad9580ab4da645cfea52a91d2da99a49a1e76616d8be68441a986fad652b0 \ + --hash=sha256:cc00b4511294f5f6b65c4e77a1a9c62f52490a63d2c120f3872176b40a82351e \ + --hash=sha256:cf896020f6a9f095a547b3d672c8db1ef2ed71fca11250731fa1d4a4cb8b1590 \ + --hash=sha256:e0fdb8322206fa02aa38f71519ff75dce2eb481b7e1110e2936795cb376bb6ee \ + --hash=sha256:277538466657ca5d6637f80be100242f9831d75138b788d718edd3aab34621f8 \ + --hash=sha256:2c77eb0560f54ce654ab82d6b2a64327a71ee969b29022bf9746ca311c9f5069 \ + --hash=sha256:755a7853b679e79d0a799351c092a9b0271f95ff54c8dd8823d8b527a2926a86 \ + --hash=sha256:77197a2d525e761cdd4c771180b4bd0d80703654c6385e4311cbbbe2beb56fa1 \ + --hash=sha256:eb8bb79d0ab00c931c8333b745f06fec481a51c52d70acd4ee95d6093ba5c386 \ + --hash=sha256:131f61de82ef28f3e20beb4bfc24f9692d28cecfd704e20e6c7f070f7793013a \ + --hash=sha256:ac35435974b2e27cd4520f29c191d7da36f4189aa3264e52c4c6c6d089ab6142 \ + --hash=sha256:04b6ea99daa2a8460728794213d76d45ad58ea247dc7e7ff148d7dd726e87863 \ + --hash=sha256:2b9442f8b4c3d575f6cc3db0e856034e0f5a9d55ecd636f52d8c496795b26952 \ + --hash=sha256:b3d3b3ecba1fe1bdb6f180770a137f877c8f07571f7b2934bb269475bcf0e5e8 \ + --hash=sha256:670a58c0d75cb0e78e73dd003bd96d4440bbb1f2bc041dcf7b81767ca4fb0ce9 \ + --hash=sha256:5af84d23bdb86b5e90aca263df1424b43f1748480bfcde3ac2a3cbe622612468 \ + --hash=sha256:ba22e8eefabdd7aca37d0c0c00d2274000d2cebb5cce9e5a710cb55bf8797b31 \ + --hash=sha256:b798b22fa7e92b439547323b8b719d217f1e1b7677585cfeeedf3b55c70bb7fb \ + --hash=sha256:59cff28af8cce96cb7e94a459726e1d88f6f5fa75097f9dcbebd99118d64ea4c \ + --hash=sha256:fe859e445abc9ba9e97950ddafb904e23234c4ecb76b0fae6c86e80592ce464a \ + --hash=sha256:655f3c474067f1e277430f23cc0549f0b1dc99b82aec6e53f80b9b2db7f76f11 \ + --hash=sha256:0ebc2be053c9a03a2f3e20a466e87bf12a51586b3c79bd2a22171b073a805346 \ + --hash=sha256:01e6e60654df64cca53733cda39446d67100c819c181d403afb120e0d2a71e1b \ + --hash=sha256:d46f4e5d455cb5563685c52ef212696f0a6cc1ea627603218eabbd8a095291d8 \ + --hash=sha256:3780b2663ee7ebb37cb83263326e3cd7f8b2ea439c448539d4b87de12c8d06ab enum34==1.1.2 \ --hash=sha256:2475d7fcddf5951e92ff546972758802de5260bf409319a9f1934e6bbc8b1dc7 \ --hash=sha256:35907defb0f992b75ab7788f65fedc1cf20ffa22688e0e6f6f12afc06b3ea501 diff --git a/letsencrypt-auto-source/pieces/pipstrap.py b/letsencrypt-auto-source/pieces/pipstrap.py index 505f8ca72..78491b5e3 100755 --- a/letsencrypt-auto-source/pieces/pipstrap.py +++ b/letsencrypt-auto-source/pieces/pipstrap.py @@ -21,6 +21,7 @@ anything goes wrong, it will exit with a non-zero status code. # 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 distutils.version import StrictVersion from hashlib import sha256 from os.path import join from pipes import quote @@ -55,12 +56,14 @@ except ImportError: from urllib.parse import urlparse # 3.4 -__version__ = 1, 1, 1 +__version__ = 1, 3, 0 +PIP_VERSION = '9.0.1' # wheel has a conditional dependency on argparse: maybe_argparse = ( - [('https://pypi.python.org/packages/source/a/argparse/' + [('https://pypi.python.org/packages/18/dd/' + 'e617cfc3f6210ae183374cd9f6a26b20514bbb5a792af97949c5aacddf0f/' 'argparse-1.4.0.tar.gz', '62b089a55be1d8949cd2bc7e0df0bddb9e028faefc8c32038cc84862aefdd6e4')] if version_info < (2, 7, 0) else []) @@ -68,13 +71,19 @@ maybe_argparse = ( 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'), + ('https://pypi.python.org/packages/11/b6/' + 'abcb525026a4be042b486df43905d6893fb04f05aac21c32c638e939e447/' + 'pip-{0}.tar.gz' + .format(PIP_VERSION), + '09f243e1a7b461f654c26a725fa373211bb7ff17a9300058b205c61658ca940d'), # This version of setuptools has only optional dependencies: - ('https://pypi.python.org/packages/source/s/setuptools/' + ('https://pypi.python.org/packages/69/65/' + '4c544cde88d4d876cdf5cbc5f3f15d02646477756d89547e9a7ecd6afa76/' 'setuptools-20.2.2.tar.gz', '24fcfc15364a9fe09a220f37d2dcedc849795e3de3e4b393ee988e66a9cbd85a'), - ('https://pypi.python.org/packages/source/w/wheel/wheel-0.29.0.tar.gz', + ('https://pypi.python.org/packages/c9/1d/' + 'bd19e691fd4cfe908c76c429fe6e4436c9e83583c4414b54f6c85471954a/' + 'wheel-0.29.0.tar.gz', '1ebb8ad7e26b448e9caa4773d2357849bf80ff9e313964bcaf79cbf0201a1648') ] @@ -124,11 +133,21 @@ def hashed_download(url, temp, digest): def main(): + pip_version = StrictVersion(check_output(['pip', '--version']) + .decode('utf-8').split()[1]) + min_pip_version = StrictVersion(PIP_VERSION) + if pip_version >= min_pip_version: + return 0 + has_pip_cache = pip_version >= StrictVersion('6.0') + 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 ' + + # Disable cache since we're not using it and it otherwise + # sometimes throws permission warnings: + ('--no-cache-dir ' if has_pip_cache else '') + ' '.join(quote(d) for d in downloads), shell=True) except HashError as exc: diff --git a/tests/boulder-integration.sh b/tests/boulder-integration.sh index 913880c8b..181dcc5fb 100755 --- a/tests/boulder-integration.sh +++ b/tests/boulder-integration.sh @@ -52,15 +52,19 @@ CheckHooks() { if [ $(head -n1 $HOOK_TEST) = "wtf.pre" ]; then echo "wtf.pre" > "$EXPECTED" echo "wtf2.pre" >> "$EXPECTED" - echo "renew" >> "$EXPECTED" - echo "renew" >> "$EXPECTED" + echo "deploy" >> "$EXPECTED" + echo "deploy" >> "$EXPECTED" + echo "deploy" >> "$EXPECTED" + echo "deploy" >> "$EXPECTED" echo "wtf.post" >> "$EXPECTED" echo "wtf2.post" >> "$EXPECTED" else echo "wtf2.pre" > "$EXPECTED" echo "wtf.pre" >> "$EXPECTED" - echo "renew" >> "$EXPECTED" - echo "renew" >> "$EXPECTED" + echo "deploy" >> "$EXPECTED" + echo "deploy" >> "$EXPECTED" + echo "deploy" >> "$EXPECTED" + echo "deploy" >> "$EXPECTED" echo "wtf2.post" >> "$EXPECTED" echo "wtf.post" >> "$EXPECTED" fi @@ -74,6 +78,49 @@ CheckHooks() { rm "$HOOK_TEST" } +# Checks if deploy is in the hook output and deletes the file +DeployInHookOutput() { + CONTENTS=$(cat "$HOOK_TEST") + rm "$HOOK_TEST" + grep deploy <(echo "$CONTENTS") +} + +# Asserts that there is a saved renew_hook for a lineage. +# +# Arguments: +# Name of lineage to check +CheckSavedRenewHook() { + if ! grep renew_hook "$config_dir/renewal/$1.conf"; then + echo "Hook wasn't saved as renew_hook" >&2 + exit 1 + fi +} + +# Asserts the deploy hook was properly run and saved and deletes the hook file +# +# Arguments: +# Lineage name of the issued cert +CheckDeployHook() { + if ! DeployInHookOutput; then + echo "The deploy hook wasn't run" >&2 + exit 1 + fi + CheckSavedRenewHook $1 +} + +# Asserts the renew hook wasn't run but was saved and deletes the hook file +# +# Arguments: +# Lineage name of the issued cert +# Asserts the deploy hook wasn't run and deletes the hook file +CheckRenewHook() { + if DeployInHookOutput; then + echo "The renew hook was incorrectly run" >&2 + exit 1 + fi + CheckSavedRenewHook $1 +} + # Cleanup coverage data coverage erase @@ -101,27 +148,43 @@ common plugins --init --prepare | grep webroot python ./tests/run_http_server.py $http_01_port & python_server_pid=$! +certname="le1.wtf" common --domains le1.wtf --preferred-challenges tls-sni-01 auth \ + --cert-name $certname \ --pre-hook 'echo wtf.pre >> "$HOOK_TEST"' \ --post-hook 'echo wtf.post >> "$HOOK_TEST"'\ - --renew-hook 'echo renew >> "$HOOK_TEST"' + --deploy-hook 'echo deploy >> "$HOOK_TEST"' kill $python_server_pid +CheckDeployHook $certname + python ./tests/run_http_server.py $tls_sni_01_port & python_server_pid=$! +certname="le2.wtf" common --domains le2.wtf --preferred-challenges http-01 run \ + --cert-name $certname \ --pre-hook 'echo wtf.pre >> "$HOOK_TEST"' \ --post-hook 'echo wtf.post >> "$HOOK_TEST"'\ - --renew-hook 'echo renew >> "$HOOK_TEST"' + --deploy-hook 'echo deploy >> "$HOOK_TEST"' kill $python_server_pid +CheckDeployHook $certname -common certonly -a manual -d le.wtf --rsa-key-size 4096 \ +certname="le.wtf" +common certonly -a manual -d le.wtf --rsa-key-size 4096 --cert-name $certname \ --manual-auth-hook ./tests/manual-http-auth.sh \ --manual-cleanup-hook ./tests/manual-http-cleanup.sh \ --pre-hook 'echo wtf2.pre >> "$HOOK_TEST"' \ - --post-hook 'echo wtf2.post >> "$HOOK_TEST"' - -common certonly -a manual -d dns.le.wtf --preferred-challenges dns,tls-sni \ - --manual-auth-hook ./tests/manual-dns-auth.sh + --post-hook 'echo wtf2.post >> "$HOOK_TEST"' \ + --renew-hook 'echo deploy >> "$HOOK_TEST"' +CheckRenewHook $certname + +certname="dns.le.wtf" +common -a manual -d dns.le.wtf --preferred-challenges dns,tls-sni run \ + --cert-name $certname \ + --manual-auth-hook ./tests/manual-dns-auth.sh \ + --pre-hook 'echo wtf2.pre >> "$HOOK_TEST"' \ + --post-hook 'echo wtf2.post >> "$HOOK_TEST"' \ + --renew-hook 'echo deploy >> "$HOOK_TEST"' +CheckRenewHook $certname common certonly --cert-name newname -d newname.le.wtf diff --git a/tests/integration/_common.sh b/tests/integration/_common.sh index 48d20eb3b..d151bdc3f 100755 --- a/tests/integration/_common.sh +++ b/tests/integration/_common.sh @@ -2,12 +2,13 @@ # the kernel to use. root=${root:-$(mktemp -d -t leitXXXX)} echo "Root integration tests directory: $root" -store_flags="--config-dir $root/conf --work-dir $root/work" +config_dir="$root/conf" +store_flags="--config-dir $config_dir --work-dir $root/work" store_flags="$store_flags --logs-dir $root/logs" tls_sni_01_port=5001 http_01_port=5002 sources="acme/,$(ls -dm certbot*/ | tr -d ' \n')" -export root store_flags tls_sni_01_port http_01_port sources +export root config_dir store_flags tls_sni_01_port http_01_port sources certbot_test () { certbot_test_no_force_renew \ @@ -85,11 +85,14 @@ deps = configobj==4.7.2 cryptography==1.2.3 enum34==0.9.23 + google-api-python-client==1.5 idna==2.0 ipaddress==1.0.16 mock==1.0.1 ndg-httpsclient==0.3.2 + oauth2client==2.0 parsedatetime==1.4 + pyasn1-modules==0.0.5 pyasn1==0.1.9 pyparsing==1.5.6 pyrfc3339==1.0 @@ -161,7 +164,7 @@ passenv = DOCKER_* commands = docker build -t certbot-compatibility-test -f certbot-compatibility-test/Dockerfile . docker build -t nginx-compat -f certbot-compatibility-test/Dockerfile-nginx . - docker run --rm -it nginx-compat -c nginx.tar.gz -vvvv + docker run --rm -it nginx-compat -c nginx.tar.gz -vv -aie whitelist_externals = docker passenv = DOCKER_* |