diff --git a/.travis.yml b/.travis.yml index 58618e5a0..8126b06b2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -33,13 +33,6 @@ matrix: - sudo cat /var/log/mysql/error.log - ps aux | grep mysql services: docker - - python: "2.6" - env: TOXENV=py26-oldest BOULDER_INTEGRATION=1 - sudo: required - after_failure: - - sudo cat /var/log/mysql/error.log - - ps aux | grep mysql - services: docker - python: "2.7" env: TOXENV=py27_install BOULDER_INTEGRATION=1 sudo: required @@ -180,4 +173,3 @@ notifications: on_success: never on_failure: always use_notice: true - skip_join: true diff --git a/CHANGELOG.md b/CHANGELOG.md index d52518195..0a33b053a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,107 @@ Certbot adheres to [Semantic Versioning](http://semver.org/). +## 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 + +* Plugins for performing DNS challenges for popular providers. Like the Apache + and Nginx plugins, these plugins are packaged separately and not included in + Certbot by default. So far, we have plugins for + [Amazon Route 53](https://pypi.python.org/pypi/certbot-dns-route53), + [Cloudflare](https://pypi.python.org/pypi/certbot-dns-cloudflare), + [DigitalOcean](https://pypi.python.org/pypi/certbot-dns-digitalocean), and + [Google Cloud](https://pypi.python.org/pypi/certbot-dns-google) which all + work on Python 2.6, 2.7, and 3.3+. Additionally, we have plugins for + [CloudXNS](https://pypi.python.org/pypi/certbot-dns-cloudxns), + [DNSimple](https://pypi.python.org/pypi/certbot-dns-dnsimple), + [NS1](https://pypi.python.org/pypi/certbot-dns-nsone) which work on Python + 2.7 and 3.3+ (and not 2.6). Currently, there isn't a good way to install + these plugins when using `certbot-auto`, but that should change soon. +* IPv6 support in the standalone plugin. When performing a challenge, the + standalone plugin automatically handles listening for IPv4/IPv6 traffic based + on the configuration of your system. +* A mechanism for keeping your Apache and Nginx SSL/TLS configuration up to + date. When the Apache or Nginx plugins are used, they place SSL/TLS + configuration options in the root of Certbot's config directory + (`/etc/letsencrypt` by default). Now when a new version of these plugins run + on your system, they will automatically update the file to the newest + version if it is unmodified. If you manually modified the file, Certbot will + display a warning giving you a path to the updated file which you can use as + a reference to manually update your modified copy. +* `--http-01-address` and `--tls-sni-01-address` flags for controlling the + address Certbot listens on when using the standalone plugin. +* The command `certbot certificates` that lists certificates managed by Certbot + now performs additional validity checks to notify you if your files have + become corrupted. + +### Changed + +* Messages custom hooks print to `stdout` are now displayed by Certbot when not + running in `--quiet` mode. +* `jwk` and `alg` fields in JWS objects have been moved into the protected + header causing Certbot to more closely follow the latest version of the ACME + spec. + +### Fixed + +* Permissions on renewal configuration files are now properly preserved when + they are updated. +* A bug causing Certbot to display strange defaults in its help output when + using Python <= 2.7.4 has been fixed. +* Certbot now properly handles mixed case domain names found in custom CSRs. +* A number of poorly worded prompts and error messages. + +### Removed + +* Support for OpenSSL 1.0.0 in `certbot-auto` has been removed as we now pin a + newer version of `cryptography` which dropped support for this version. + +More details about these changes can be found on our GitHub repo: +https://github.com/certbot/certbot/issues?q=is%3Aissue+milestone%3A0.15.0+is%3Aclosed + ## 0.14.2 - 2017-05-25 ### Fixed diff --git a/README.rst b/README.rst index ff6fafe36..b96421e7a 100644 --- a/README.rst +++ b/README.rst @@ -132,8 +132,8 @@ Current Features * Supports multiple web servers: - - apache/2.x (beta support for auto-configuration) - - nginx/0.8.48+ (alpha support for auto-configuration, beta support in 0.14.0) + - apache/2.x + - nginx/0.8.48+ - webroot (adds files to webroot directories in order to prove control of domains and obtain certs) - standalone (runs its own simple webserver to prove you control a domain) 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 f86a9971a..de15284c0 100644 --- a/acme/acme/crypto_util.py +++ b/acme/acme/crypto_util.py @@ -107,7 +107,7 @@ class SSLSocket(object): # pylint: disable=too-few-public-methods def probe_sni(name, host, port=443, timeout=300, - method=_DEFAULT_TLSSNI01_SSL_METHOD, source_address=('0', 0)): + method=_DEFAULT_TLSSNI01_SSL_METHOD, source_address=('', 0)): """Probe SNI server for SSL certificate. :param bytes name: Byte string to send as the server name in the @@ -132,9 +132,14 @@ def probe_sni(name, host, port=443, timeout=300, socket_kwargs = {} if sys.version_info < (2, 7) else { 'source_address': source_address} + host_protocol_agnostic = None if host == '::' or host == '0' else host + try: # pylint: disable=star-args - sock = socket.create_connection((host, port), **socket_kwargs) + logger.debug("Attempting to connect to %s:%d%s.", host_protocol_agnostic, port, + " from {0}:{1}".format(source_address[0], source_address[1]) if \ + socket_kwargs else "") + sock = socket.create_connection((host_protocol_agnostic, port), **socket_kwargs) except socket.error as error: raise errors.Error(error) @@ -213,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/standalone.py b/acme/acme/standalone.py index 087240c15..c221f5883 100644 --- a/acme/acme/standalone.py +++ b/acme/acme/standalone.py @@ -4,7 +4,9 @@ import collections import functools import logging import os +import socket import sys +import threading from six.moves import BaseHTTPServer # type: ignore # pylint: disable=import-error from six.moves import http_client # pylint: disable=import-error @@ -26,6 +28,11 @@ class TLSServer(socketserver.TCPServer): """Generic TLS Server.""" def __init__(self, *args, **kwargs): + self.ipv6 = kwargs.pop("ipv6", False) + if self.ipv6: + self.address_family = socket.AF_INET6 + else: + self.address_family = socket.AF_INET self.certs = kwargs.pop("certs", {}) self.method = kwargs.pop( # pylint: disable=protected-access @@ -49,12 +56,81 @@ class ACMEServerMixin: # pylint: disable=old-style-class allow_reuse_address = True +class BaseDualNetworkedServers(object): + """Base class for a pair of IPv6 and IPv4 servers that tries to do everything + it's asked for both servers, but where failures in one server don't + affect the other. + + If two servers are instantiated, they will serve on the same port. + """ + + def __init__(self, ServerClass, server_address, *remaining_args, **kwargs): + port = server_address[1] + self.threads = [] + self.servers = [] + + # Must try True first. + # Ubuntu, for example, will fail to bind to IPv4 if we've already bound + # to IPv6. But that's ok, since it will accept IPv4 connections on the IPv6 + # socket. On the other hand, FreeBSD will successfully bind to IPv4 on the + # same port, which means that server will accept the IPv4 connections. + # If Python is compiled without IPv6, we'll error out but (probably) successfully + # create the IPv4 server. + for ip_version in [True, False]: + try: + kwargs["ipv6"] = ip_version + new_address = (server_address[0],) + (port,) + server_address[2:] + new_args = (new_address,) + remaining_args + server = ServerClass(*new_args, **kwargs) # pylint: disable=star-args + except socket.error: + logger.debug("Failed to bind to %s:%s using %s", new_address[0], + new_address[1], "IPv6" if ip_version else "IPv4") + else: + self.servers.append(server) + # If two servers are set up and port 0 was passed in, ensure we always + # bind to the same port for both servers. + port = server.socket.getsockname()[1] + if len(self.servers) == 0: + raise socket.error("Could not bind to IPv4 or IPv6.") + + def serve_forever(self): + """Wraps socketserver.TCPServer.serve_forever""" + for server in self.servers: + thread = threading.Thread( + # pylint: disable=no-member + target=server.serve_forever) + thread.start() + self.threads.append(thread) + + def getsocknames(self): + """Wraps socketserver.TCPServer.socket.getsockname""" + return [server.socket.getsockname() for server in self.servers] + + def shutdown_and_server_close(self): + """Wraps socketserver.TCPServer.shutdown, socketserver.TCPServer.server_close, and + threading.Thread.join""" + for server in self.servers: + server.shutdown() + server.server_close() + for thread in self.threads: + thread.join() + self.threads = [] + + class TLSSNI01Server(TLSServer, ACMEServerMixin): """TLSSNI01 Server.""" - def __init__(self, server_address, certs): + def __init__(self, server_address, certs, ipv6=False): TLSServer.__init__( - self, server_address, BaseRequestHandlerWithLogging, certs=certs) + self, server_address, BaseRequestHandlerWithLogging, certs=certs, ipv6=ipv6) + + +class TLSSNI01DualNetworkedServers(BaseDualNetworkedServers): + """TLSSNI01Server Wrapper. Tries everything for both. Failures for one don't + affect the other.""" + + def __init__(self, *args, **kwargs): + BaseDualNetworkedServers.__init__(self, TLSSNI01Server, *args, **kwargs) class BaseRequestHandlerWithLogging(socketserver.BaseRequestHandler): @@ -70,13 +146,33 @@ class BaseRequestHandlerWithLogging(socketserver.BaseRequestHandler): socketserver.BaseRequestHandler.handle(self) -class HTTP01Server(BaseHTTPServer.HTTPServer, ACMEServerMixin): +class HTTPServer(BaseHTTPServer.HTTPServer): + """Generic HTTP Server.""" + + def __init__(self, *args, **kwargs): + self.ipv6 = kwargs.pop("ipv6", False) + if self.ipv6: + self.address_family = socket.AF_INET6 + else: + self.address_family = socket.AF_INET + BaseHTTPServer.HTTPServer.__init__(self, *args, **kwargs) + + +class HTTP01Server(HTTPServer, ACMEServerMixin): """HTTP01 Server.""" - def __init__(self, server_address, resources): - BaseHTTPServer.HTTPServer.__init__( + def __init__(self, server_address, resources, ipv6=False): + HTTPServer.__init__( self, server_address, HTTP01RequestHandler.partial_init( - simple_http_resources=resources)) + simple_http_resources=resources), ipv6=ipv6) + + +class HTTP01DualNetworkedServers(BaseDualNetworkedServers): + """HTTP01Server Wrapper. Tries everything for both. Failures for one don't + affect the other.""" + + def __init__(self, *args, **kwargs): + BaseDualNetworkedServers.__init__(self, HTTP01Server, *args, **kwargs) class HTTP01RequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): diff --git a/acme/acme/standalone_test.py b/acme/acme/standalone_test.py index 613258c97..16669680c 100644 --- a/acme/acme/standalone_test.py +++ b/acme/acme/standalone_test.py @@ -1,6 +1,7 @@ """Tests for acme.standalone.""" import os import shutil +import socket import threading import tempfile import time @@ -9,6 +10,7 @@ import unittest from six.moves import http_client # pylint: disable=import-error from six.moves import socketserver # type: ignore # pylint: disable=import-error +import mock import requests from acme import challenges @@ -29,6 +31,13 @@ class TLSServerTest(unittest.TestCase): ('', 0), socketserver.BaseRequestHandler, bind_and_activate=True) server.server_close() # pylint: disable=no-member + def test_ipv6(self): + if socket.has_ipv6: + from acme.standalone import TLSServer + server = TLSServer( + ('', 0), socketserver.BaseRequestHandler, bind_and_activate=True, ipv6=True) + server.server_close() # pylint: disable=no-member + class TLSSNI01ServerTest(unittest.TestCase): """Test for acme.standalone.TLSSNI01Server.""" @@ -112,6 +121,136 @@ class HTTP01ServerTest(unittest.TestCase): self.assertFalse(self._test_http01(add=False)) +class BaseDualNetworkedServersTest(unittest.TestCase): + """Test for acme.standalone.BaseDualNetworkedServers.""" + + _multiprocess_can_split_ = True + + class SingleProtocolServer(socketserver.TCPServer): + """Server that only serves on a single protocol. FreeBSD has this behavior for AF_INET6.""" + def __init__(self, *args, **kwargs): + ipv6 = kwargs.pop("ipv6", False) + if ipv6: + self.address_family = socket.AF_INET6 + kwargs["bind_and_activate"] = False + else: + self.address_family = socket.AF_INET + socketserver.TCPServer.__init__(self, *args, **kwargs) + if ipv6: + # pylint: disable=no-member + self.socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 1) + try: + self.server_bind() + self.server_activate() + except: + self.server_close() + raise + + @mock.patch("socket.socket.bind") + def test_fail_to_bind(self, mock_bind): + mock_bind.side_effect = socket.error + from acme.standalone import BaseDualNetworkedServers + self.assertRaises(socket.error, BaseDualNetworkedServers, + BaseDualNetworkedServersTest.SingleProtocolServer, + ("", 0), + socketserver.BaseRequestHandler) + + def test_ports_equal(self): + from acme.standalone import BaseDualNetworkedServers + servers = BaseDualNetworkedServers( + BaseDualNetworkedServersTest.SingleProtocolServer, + ("", 0), + socketserver.BaseRequestHandler) + socknames = servers.getsocknames() + prev_port = None + # assert ports are equal + for sockname in socknames: + port = sockname[1] + if prev_port: + self.assertEqual(prev_port, port) + prev_port = port + + +class TLSSNI01DualNetworkedServersTest(unittest.TestCase): + """Test for acme.standalone.TLSSNI01DualNetworkedServers.""" + + _multiprocess_can_split_ = True + + def setUp(self): + self.certs = {b'localhost': ( + test_util.load_pyopenssl_private_key('rsa2048_key.pem'), + test_util.load_cert('rsa2048_cert.pem'), + )} + from acme.standalone import TLSSNI01DualNetworkedServers + self.servers = TLSSNI01DualNetworkedServers(("", 0), certs=self.certs) + self.servers.serve_forever() + + def tearDown(self): + self.servers.shutdown_and_server_close() + + def test_connect(self): + socknames = self.servers.getsocknames() + # connect to all addresses + for sockname in socknames: + host, port = sockname[:2] + cert = crypto_util.probe_sni( + b'localhost', host=host, port=port, timeout=1) + self.assertEqual(jose.ComparableX509(cert), + jose.ComparableX509(self.certs[b'localhost'][1])) + + +class HTTP01DualNetworkedServersTest(unittest.TestCase): + """Tests for acme.standalone.HTTP01DualNetworkedServers.""" + + _multiprocess_can_split_ = True + + def setUp(self): + self.account_key = jose.JWK.load( + test_util.load_vector('rsa1024_key.pem')) + self.resources = set() + + from acme.standalone import HTTP01DualNetworkedServers + self.servers = HTTP01DualNetworkedServers(('', 0), resources=self.resources) + + # pylint: disable=no-member + self.port = self.servers.getsocknames()[0][1] + self.servers.serve_forever() + + def tearDown(self): + self.servers.shutdown_and_server_close() + + def test_index(self): + response = requests.get( + 'http://localhost:{0}'.format(self.port), verify=False) + self.assertEqual( + response.text, 'ACME client standalone challenge solver') + self.assertTrue(response.ok) + + def test_404(self): + response = requests.get( + 'http://localhost:{0}/foo'.format(self.port), verify=False) + self.assertEqual(response.status_code, http_client.NOT_FOUND) + + def _test_http01(self, add): + chall = challenges.HTTP01(token=(b'x' * 16)) + response, validation = chall.response_and_validation(self.account_key) + + from acme.standalone import HTTP01RequestHandler + resource = HTTP01RequestHandler.HTTP01Resource( + chall=chall, response=response, validation=validation) + if add: + self.resources.add(resource) + return resource.response.simple_verify( + resource.chall, 'localhost', self.account_key.public_key(), + port=self.port) + + def test_http01_found(self): + self.assertTrue(self._test_http01(add=True)) + + def test_http01_not_found(self): + self.assertFalse(self._test_http01(add=False)) + + class TestSimpleTLSSNI01Server(unittest.TestCase): """Tests for acme.standalone.simple_tls_sni_01_server.""" @@ -137,7 +276,6 @@ class TestSimpleTLSSNI01Server(unittest.TestCase): ) self.old_cwd = os.getcwd() os.chdir(self.test_cwd) - self.thread.start() def tearDown(self): os.chdir(self.old_cwd) @@ -146,13 +284,12 @@ class TestSimpleTLSSNI01Server(unittest.TestCase): def test_it(self): max_attempts = 5 - while max_attempts: - max_attempts -= 1 + for attempt in range(max_attempts): try: cert = crypto_util.probe_sni( b'localhost', b'0.0.0.0', self.port) except errors.Error: - self.assertTrue(max_attempts > 0, "Timeout!") + self.assertTrue(attempt + 1 < max_attempts, "Timeout!") time.sleep(1) # wait until thread starts else: self.assertEqual(jose.ComparableX509(cert), @@ -160,6 +297,11 @@ class TestSimpleTLSSNI01Server(unittest.TestCase): 'rsa2048_cert.pem')) break + if attempt == 0: + # the first attempt is always meant to fail, so we can test + # the socket failure code-path for probe_sni, as well + self.thread.start() + if __name__ == "__main__": unittest.main() # pragma: no cover 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 0938c5c68..0b166dd91 100644 --- a/acme/setup.py +++ b/acme/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.15.0.dev0' +version = '0.17.0.dev0' # Please update tox.ini when modifying dependency version requirements install_requires = [ @@ -16,11 +16,7 @@ install_requires = [ 'PyOpenSSL>=0.13', 'pyrfc3339', 'pytz', - # requests>=2.10 is required to fix - # https://github.com/shazow/urllib3/issues/556. This requirement can be - # relaxed to 'requests[security]>=2.4.1', however, less useful errors - # will be raised for some network/SSL errors. - 'requests[security]>=2.10', + 'requests[security]>=2.4.1', # security extras added in 2.4.1 # For pkg_resources. >=1.0 so pip resolves it to a version cryptography # will tolerate; see #2599: 'setuptools>=1.0', diff --git a/certbot-apache/certbot_apache/centos-options-ssl-apache.conf b/certbot-apache/certbot_apache/centos-options-ssl-apache.conf index fbe8da0f2..17ae1be76 100644 --- a/certbot-apache/certbot_apache/centos-options-ssl-apache.conf +++ b/certbot-apache/certbot_apache/centos-options-ssl-apache.conf @@ -1,4 +1,8 @@ -# Baseline setting to Include for SSL sites +# This file contains important security parameters. If you modify this file +# manually, Certbot will be unable to automatically provide future security +# updates. Instead, Certbot will print and log an error message with a path to +# the up-to-date file that you will need to refer to when manually updating +# this file. SSLEngine on diff --git a/certbot-apache/certbot_apache/configurator.py b/certbot-apache/certbot_apache/configurator.py index 13b325d7f..9e3eb4139 100644 --- a/certbot-apache/certbot_apache/configurator.py +++ b/certbot-apache/certbot_apache/configurator.py @@ -5,7 +5,6 @@ import fnmatch import logging import os import re -import shutil import socket import time @@ -145,6 +144,11 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): return os.path.join(self.config.config_dir, constants.MOD_SSL_CONF_DEST) + @property + def updated_mod_ssl_conf_digest(self): + """Full absolute path to digest of updated SSL configuration file.""" + return os.path.join(self.config.config_dir, constants.UPDATED_MOD_SSL_CONF_DIGEST) + def prepare(self): """Prepare the authenticator/installer. @@ -195,7 +199,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): # Get all of the available vhosts self.vhosts = self.get_virtual_hosts() - install_ssl_options_conf(self.mod_ssl_conf) + install_ssl_options_conf(self.mod_ssl_conf, self.updated_mod_ssl_conf_digest) # Prevent two Apache plugins from modifying a config at once try: @@ -343,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) @@ -1981,19 +1983,11 @@ def _split_aug_path(vhost_path): return file_path, "/".join(reversed(internal_path)) -def install_ssl_options_conf(options_ssl): - """ - Copy Certbot's SSL options file into the system's config dir if - required. - """ +def install_ssl_options_conf(options_ssl, options_ssl_digest): + """Copy Certbot's SSL options file into the system's config dir if required.""" + # XXX if we ever try to enforce a local privilege boundary (eg, running # certbot for unprivileged users via setuid), this function will need # to be modified. - - # XXX if the user is in security-autoupdate mode, we should be willing to - # overwrite the options_ssl file at least if it's unmodified: - # https://github.com/letsencrypt/letsencrypt/issues/1123 - - # Check to make sure options-ssl.conf is installed - if not os.path.isfile(options_ssl): - shutil.copyfile(constants.os_constant("MOD_SSL_CONF_SRC"), options_ssl) + return common.install_ssl_options_conf(options_ssl, options_ssl_digest, + constants.os_constant("MOD_SSL_CONF_SRC"), constants.ALL_SSL_OPTIONS_HASHES) diff --git a/certbot-apache/certbot_apache/constants.py b/certbot-apache/certbot_apache/constants.py index 3cfeb4dd6..0c5f7263a 100644 --- a/certbot-apache/certbot_apache/constants.py +++ b/certbot-apache/certbot_apache/constants.py @@ -110,6 +110,24 @@ CLI_DEFAULTS_SUSE = dict( MOD_SSL_CONF_SRC=pkg_resources.resource_filename( "certbot_apache", "options-ssl-apache.conf") ) +CLI_DEFAULTS_ARCH = dict( + server_root="/etc/httpd", + vhost_root="/etc/httpd/conf", + vhost_files="*.conf", + logs_root="/var/log/httpd", + version_cmd=['apachectl', '-v'], + define_cmd=['apachectl', '-t', '-D', 'DUMP_RUN_CFG'], + restart_cmd=['apachectl', 'graceful'], + conftest_cmd=['apachectl', 'configtest'], + enmod=None, + dismod=None, + le_vhost_ext="-le-ssl.conf", + handle_mods=False, + handle_sites=False, + challenge_location="/etc/httpd/conf", + MOD_SSL_CONF_SRC=pkg_resources.resource_filename( + "certbot_apache", "options-ssl-apache.conf") +) CLI_DEFAULTS = { "default": CLI_DEFAULTS_DEFAULT, "debian": CLI_DEFAULTS_DEBIAN, @@ -125,12 +143,27 @@ CLI_DEFAULTS = { "darwin": CLI_DEFAULTS_DARWIN, "opensuse": CLI_DEFAULTS_SUSE, "suse": CLI_DEFAULTS_SUSE, + "arch": CLI_DEFAULTS_ARCH, } """CLI defaults.""" MOD_SSL_CONF_DEST = "options-ssl-apache.conf" """Name of the mod_ssl config file as saved in `IConfig.config_dir`.""" + +UPDATED_MOD_SSL_CONF_DIGEST = ".updated-options-ssl-apache-conf-digest.txt" +"""Name of the hash of the updated or informed mod_ssl_conf as saved in `IConfig.config_dir`.""" + +ALL_SSL_OPTIONS_HASHES = [ + '2086bca02db48daf93468332543c60ac6acdb6f0b58c7bfdf578a5d47092f82a', + '4844d36c9a0f587172d9fa10f4f1c9518e3bcfa1947379f155e16a70a728c21a', + '5a922826719981c0a234b1fbcd495f3213e49d2519e845ea0748ba513044b65b', + '4066b90268c03c9ba0201068eaa39abbc02acf9558bb45a788b630eb85dadf27', + 'f175e2e7c673bd88d0aff8220735f385f916142c44aa83b09f1df88dd4767a88', + 'cfdd7c18d2025836ea3307399f509cfb1ebf2612c87dd600a65da2a8e2f2797b', +] +"""SHA256 hashes of the contents of previous versions of all versions of MOD_SSL_CONF_SRC""" + AUGEAS_LENS_DIR = pkg_resources.resource_filename( "certbot_apache", "augeas_lens") """Path to the Augeas lens directory""" diff --git a/certbot-apache/certbot_apache/display_ops.py b/certbot-apache/certbot_apache/display_ops.py index 6bcb64dd5..7aec26f81 100644 --- a/certbot-apache/certbot_apache/display_ops.py +++ b/certbot-apache/certbot_apache/display_ops.py @@ -27,9 +27,7 @@ def select_vhost(domain, vhosts): return None while True: code, tag = _vhost_menu(domain, vhosts) - if code == display_util.HELP: - _more_info_vhost(vhosts[tag]) - elif code == display_util.OK: + if code == display_util.OK: return vhosts[tag] else: return None @@ -85,21 +83,14 @@ def _vhost_menu(domain, vhosts): "or Address of {0}.{1}Which virtual host would you " "like to choose?\n(note: conf files with multiple " "vhosts are not yet supported)".format(domain, os.linesep), - choices, help_label="More Info", - ok_label="Select", force_interactive=True) + 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) return code, tag - - -def _more_info_vhost(vhost): - zope.component.getUtility(interfaces.IDisplay).notification( - "Virtual Host Information:{0}{1}{0}{2}".format( - os.linesep, "-" * (display_util.WIDTH - 4), str(vhost)), - force_interactive=True) diff --git a/certbot-apache/certbot_apache/options-ssl-apache.conf b/certbot-apache/certbot_apache/options-ssl-apache.conf index ec07a4ba3..950a02a8b 100644 --- a/certbot-apache/certbot_apache/options-ssl-apache.conf +++ b/certbot-apache/certbot_apache/options-ssl-apache.conf @@ -1,4 +1,8 @@ -# Baseline setting to Include for SSL sites +# This file contains important security parameters. If you modify this file +# manually, Certbot will be unable to automatically provide future security +# updates. Instead, Certbot will print and log an error message with a path to +# the up-to-date file that you will need to refer to when manually updating +# this file. SSLEngine on diff --git a/certbot-apache/certbot_apache/tests/configurator_test.py b/certbot-apache/certbot_apache/tests/configurator_test.py index 75589dce5..db04bfcd1 100644 --- a/certbot-apache/certbot_apache/tests/configurator_test.py +++ b/certbot-apache/certbot_apache/tests/configurator_test.py @@ -12,6 +12,7 @@ import six # pylint: disable=unused-import from acme import challenges from certbot import achallenges +from certbot import crypto_util from certbot import errors from certbot.tests import acme_util @@ -826,8 +827,10 @@ class MultipleVhostsTest(util.ApacheTest): def test_install_ssl_options_conf(self): from certbot_apache.configurator import install_ssl_options_conf path = os.path.join(self.work_dir, "test_it") - install_ssl_options_conf(path) + other_path = os.path.join(self.work_dir, "other_test_it") + install_ssl_options_conf(path, other_path) self.assertTrue(os.path.isfile(path)) + self.assertTrue(os.path.isfile(other_path)) # TEST ENHANCEMENTS def test_supported_enhancements(self): @@ -1432,5 +1435,83 @@ class MultiVhostsTest(util.ApacheTest): mock.ANY) +class InstallSslOptionsConfTest(util.ApacheTest): + """Test that the options-ssl-nginx.conf file is installed and updated properly.""" + + def setUp(self): # pylint: disable=arguments-differ + super(InstallSslOptionsConfTest, self).setUp() + + self.config = util.get_apache_configurator( + self.config_path, self.vhost_path, self.config_dir, self.work_dir) + + def _call(self): + from certbot_apache.configurator import install_ssl_options_conf + install_ssl_options_conf(self.config.mod_ssl_conf, self.config.updated_mod_ssl_conf_digest) + + def _current_ssl_options_hash(self): + return crypto_util.sha256sum(constants.os_constant("MOD_SSL_CONF_SRC")) + + def _assert_current_file(self): + self.assertTrue(os.path.isfile(self.config.mod_ssl_conf)) + self.assertEqual(crypto_util.sha256sum(self.config.mod_ssl_conf), + self._current_ssl_options_hash()) + + def test_no_file(self): + # prepare should have placed a file there + self._assert_current_file() + os.remove(self.config.mod_ssl_conf) + self.assertFalse(os.path.isfile(self.config.mod_ssl_conf)) + self._call() + self._assert_current_file() + + def test_current_file(self): + self._assert_current_file() + self._call() + self._assert_current_file() + + def test_prev_file_updates_to_current(self): + from certbot_apache.constants import ALL_SSL_OPTIONS_HASHES + ALL_SSL_OPTIONS_HASHES.insert(0, "test_hash_does_not_match") + with mock.patch('certbot.crypto_util.sha256sum') as mock_sha256: + mock_sha256.return_value = ALL_SSL_OPTIONS_HASHES[0] + self._call() + self._assert_current_file() + + def test_manually_modified_current_file_does_not_update(self): + with open(self.config.mod_ssl_conf, "a") as mod_ssl_conf: + mod_ssl_conf.write("a new line for the wrong hash\n") + with mock.patch("certbot.plugins.common.logger") as mock_logger: + self._call() + self.assertFalse(mock_logger.warning.called) + self.assertTrue(os.path.isfile(self.config.mod_ssl_conf)) + self.assertEqual(crypto_util.sha256sum(constants.os_constant("MOD_SSL_CONF_SRC")), + self._current_ssl_options_hash()) + self.assertNotEqual(crypto_util.sha256sum(self.config.mod_ssl_conf), + self._current_ssl_options_hash()) + + def test_manually_modified_past_file_warns(self): + with open(self.config.mod_ssl_conf, "a") as mod_ssl_conf: + mod_ssl_conf.write("a new line for the wrong hash\n") + with open(self.config.updated_mod_ssl_conf_digest, "w") as f: + f.write("hashofanoldversion") + with mock.patch("certbot.plugins.common.logger") as mock_logger: + self._call() + self.assertEqual(mock_logger.warning.call_args[0][0], + "%s has been manually modified; updated ssl configuration options " + "saved to %s. We recommend updating %s for security purposes.") + self.assertEqual(crypto_util.sha256sum(constants.os_constant("MOD_SSL_CONF_SRC")), + self._current_ssl_options_hash()) + # only print warning once + with mock.patch("certbot.plugins.common.logger") as mock_logger: + self._call() + self.assertFalse(mock_logger.warning.called) + + def test_current_file_hash_in_all_hashes(self): + from certbot_apache.constants import ALL_SSL_OPTIONS_HASHES + self.assertTrue(self._current_ssl_options_hash() in ALL_SSL_OPTIONS_HASHES, + "Constants.ALL_SSL_OPTIONS_HASHES must be appended" + " with the sha256 hash of self.config.mod_ssl_conf when it is updated.") + + if __name__ == "__main__": unittest.main() # pragma: no cover diff --git a/certbot-apache/certbot_apache/tests/display_ops_test.py b/certbot-apache/certbot_apache/tests/display_ops_test.py index f8b75022e..e59d411bd 100644 --- a/certbot-apache/certbot_apache/tests/display_ops_test.py +++ b/certbot-apache/certbot_apache/tests/display_ops_test.py @@ -43,13 +43,10 @@ class SelectVhostTest(unittest.TestCase): @certbot_util.patch_get_utility() def test_more_info_cancel(self, mock_util): mock_util().menu.side_effect = [ - (display_util.HELP, 1), - (display_util.HELP, 0), (display_util.CANCEL, -1), ] self.assertEqual(None, self._call(self.vhosts)) - self.assertEqual(mock_util().notification.call_count, 2) def test_no_vhosts(self): self.assertEqual(self._call([]), None) 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 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 ccd7cbc2a..7f61f9041 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.15.0.dev0' +version = '0.17.0.dev0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot-auto b/certbot-auto index c88c96a5b..599538891 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.14.2" +LE_AUTO_VERSION="0.16.0" BASENAME=$(basename $0) USAGE="Usage: $BASENAME [OPTIONS] A self-updating wrapper script for the Certbot ACME client. When run, updates @@ -694,7 +694,7 @@ if [ "$1" = "--le-auto-phase2" ]; then # Hashin example: # pip install hashin -# hashin -r letsencrypt-auto-requirements.txt cryptography==1.5.2 +# hashin -r dependency-requirements.txt cryptography==1.5.2 # sets the new certbot-auto pinned version of cryptography to 1.5.2 argparse==1.4.0 \ @@ -707,59 +707,78 @@ pycparser==2.14 \ --hash=sha256:7959b4a74abdc27b312fed1c21e6caf9309ce0b29ea86b591fd2e99ecdf27f73 \ --no-binary pycparser -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 +asn1crypto==0.22.0 \ + --hash=sha256:d232509fefcfcdb9a331f37e9c9dc20441019ad927c7d2176cf18ed5da0ba097 \ + --hash=sha256:cbbadd640d3165ab24b06ef25d1dca09a3441611ac15f6a6b452474fdf0aed1a +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.5.3 \ - --hash=sha256:e514d92086246b53ae9b048df652cf3036b462e50a6ce9fac6b6253502679991 \ - --hash=sha256:10ee414f4b5af403a0d8f20dfa80f7dad1fc7ae5452ec5af03712d5b6e78c664 \ - --hash=sha256:7234456d1f4345a144ed07af2416c7c0659d4bb599dd1a963103dc8c183b370e \ - --hash=sha256:d3b9587406f94642bd70b3d666b813f446e95f84220c9e416ad94cbfb6be2eaa \ - --hash=sha256:b15fc6b59f1474eef62207c85888afada8acc47fae8198ba2b0197d54538961a \ - --hash=sha256:3b62d65d342704fc07ed171598db2a2775bdf587b1b6abd2cba2261bfe3ccde3 \ - --hash=sha256:059343022ec904c867a13bc55d2573e36c8cfb2c250e30d8a2e9825f253b07ba \ - --hash=sha256:c7897cf13bc8b4ee0215d83cbd51766d87c06b277fcca1f9108595508e5bcfb4 \ - --hash=sha256:9b69e983e5bf83039ddd52e52a28c7faedb2b22bdfb5876377b95aac7d3be63e \ - --hash=sha256:61e40905c426d02b3fae38088dc66ce4ef84830f7eb223dec6b3ac3ccdc676fb \ - --hash=sha256:00783a32bcd91a12177230d35bfcf70a2333ade4a6b607fac94a633a7971c671 \ - --hash=sha256:d11973f49b648cde1ea1a30e496d7557dbfeccd08b3cd9ba58d286a9c274ff8e \ - --hash=sha256:f24bedf28b81932ba6063aec9a826669f5237ea3b755efe04d98b072faa053a5 \ - --hash=sha256:3ab5725367239e3deb9b92e917aa965af3fef008f25b96a3000821869e208181 \ - --hash=sha256:8a53209de822e22b5f73bf4b99e68ac4ccc91051fd6751c8252982983e86a77d \ - --hash=sha256:5a07439d4b1e4197ac202b7eea45e26a6fd65757652dc50f1a63367f711df933 \ - --hash=sha256:26b1c4b40aec7b0074bceabe6e06565aa28176eca7323a31df66ebf89fe916d3 \ - --hash=sha256:eaa4a7b5a6682adcf8d6ebb2a08a008802657643655bb527c95c8a3860253d8e \ - --hash=sha256:8156927dcf8da274ff205ad0612f75c380df45385bacf98531a5b3348c88d135 \ - --hash=sha256:61ec0d792749d0e91e84b1d58b6dfd204806b10b5811f846c2ceca0de028c53a \ - --hash=sha256:26330c88041569ca621cc42274d0ea2667a48b6deab41467272c3aba0b6e8f07 \ - --hash=sha256:cf82ddac919b587f5e44247579b433224cc2e03332d2ea4d89aa70d7e6b64ae5 +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 enum34==1.1.2 \ --hash=sha256:2475d7fcddf5951e92ff546972758802de5260bf409319a9f1934e6bbc8b1dc7 \ --hash=sha256:35907defb0f992b75ab7788f65fedc1cf20ffa22688e0e6f6f12afc06b3ea501 -funcsigs==0.4 \ - --hash=sha256:ff5ad9e2f8d9e5d1e8bbfbcf47722ab527cf0d51caeeed9da6d0f40799383fde \ - --hash=sha256:d83ce6df0b0ea6618700fe1db353526391a8a3ada1b7aba52fed7a61da772033 -idna==2.0 \ - --hash=sha256:9b2fc50bd3c4ba306b9651b69411ef22026d4d8335b93afc2214cef1246ce707 \ - --hash=sha256:16199aad938b290f5be1057c0e1efc6546229391c23cea61ca940c115f7d3d3b +funcsigs==1.0.2 \ + --hash=sha256:330cc27ccbf7f1e992e69fef78261dc7c6569012cf397db8d3de0234e6c937ca \ + --hash=sha256:a7bb0f2cf3a3fd1ab2732cb49eba4252c2af4240442415b4abce3b87022a8f50 +idna==2.5 \ + --hash=sha256:cc19709fd6d0cbfed39ea875d29ba6d4e22c0cebc510a76d6302a28385e8bb70 \ + --hash=sha256:3cb5ce08046c4e3a560fc02f138d0ac63e00f8ce5901a56b32ec8b7994082aab ipaddress==1.0.16 \ --hash=sha256:935712800ce4760701d89ad677666cd52691fd2f6f0b340c8b4239a3c17988a5 \ --hash=sha256:5a3182b322a706525c46282ca6f064d27a02cffbd449f9f47416f1dc96aa71b0 @@ -768,24 +787,15 @@ linecache2==1.0.0 \ --hash=sha256:4b26ff4e7110db76eeb6f5a7b64a82623839d595c2038eeda662f2a2db78e97c ordereddict==1.1 \ --hash=sha256:1c35b4ac206cef2d24816c89f89cf289dd3d38cf7c449bb3fab7bf6d43f01b1f +packaging==16.8 \ + --hash=sha256:99276dc6e3a7851f32027a68f1095cd3f77c148091b092ea867a351811cfe388 \ + --hash=sha256:5d50835fdf0a7edf0b55e311b7c887786504efea1177abd7e69329a8e5ea619e parsedatetime==2.1 \ --hash=sha256:ce9d422165cf6e963905cd5f74f274ebf7cc98c941916169178ef93f0e557838 \ --hash=sha256:17c578775520c99131634e09cfca5a05ea9e1bd2a05cd06967ebece10df7af2d pbr==1.8.1 \ --hash=sha256:46c8db75ae75a056bd1cc07fa21734fe2e603d11a07833ecc1eeb74c35c72e0c \ --hash=sha256:e2127626a91e6c885db89668976db31020f0af2da728924b56480fc7ccf09649 -pyasn1==0.1.9 \ - --hash=sha256:61f9d99e3cef65feb1bfe3a2eef7a93eb93819d345bf54bcd42f4e63d5204dae \ - --hash=sha256:1802a6dd32045e472a419db1441aecab469d33e0d2749e192abdec52101724af \ - --hash=sha256:35025cd9422c96504912f04e2f15fe79390a8597b430c2ca5d0534cf9309ffa0 \ - --hash=sha256:2f96ed5a0c329ca16230b326ca12b7461ec8f65e0be3e4f997516f36bf82a345 \ - --hash=sha256:28fee44217991cfad9e6a0b9f7e3f26041e21ebc96629e94e585ccd05d49fa65 \ - --hash=sha256:326e7a854a17fab07691204747695f8f692d674588a355c441fb14f660bf4e68 \ - --hash=sha256:cda5a90485709ca6795c86056c3e5fe7266028b05e53f1d527fdf93a6365a6b8 \ - --hash=sha256:0cb2a14742b543fdd68f931a14ce3829186ed2b1b2267a06787388c96b2dd9be \ - --hash=sha256:5191ff6b9126d2c039dd87f8ff025bed274baf07fa78afa46f556b1ad7265d6e \ - --hash=sha256:8323e03637b2d072cc7041300bac6ec448c3c28950ab40376036788e9a1af629 \ - --hash=sha256:853cacd96d1f701ddd67aa03ecc05f51890135b7262e922710112f12a2ed2a7f pyOpenSSL==16.2.0 \ --hash=sha256:26ca380ddf272f7556e48064bbcd5bd71f83dfc144f3583501c7ddbd9434ee17 \ --hash=sha256:7779a3bbb74e79db234af6a08775568c6769b5821faecf6e2f4143edb227516e @@ -851,27 +861,33 @@ zope.interface==4.1.3 \ --hash=sha256:928138365245a0e8869a5999fbcc2a45475a0a6ed52a494d60dbdc540335fedd \ --hash=sha256:0d841ba1bb840eea0e6489dc5ecafa6125554971f53b5acb87764441e61bceba \ --hash=sha256:b09c8c1d47b3531c400e0195697f1414a63221de6ef478598a4f1460f7d9a392 -mock==1.0.1 \ - --hash=sha256:b839dd2d9c117c701430c149956918a423a9863b48b09c90e30a6013e7d2f44f \ - --hash=sha256:8f83080daa249d036cbccfb8ae5cc6ff007b88d6d937521371afabe7b19badbc +mock==2.0.0 \ + --hash=sha256:5ce3c71c5545b472da17b72268978914d0252980348636840bd34a00b5cc96c1 \ + --hash=sha256:b158b6df76edd239b8208d481dc46b6afd45a846b7812ff0ce58971cf5bc8bba + +# Contains the requirements for the letsencrypt package. +# +# Since the letsencrypt package depends on certbot and using pip with hashes +# requires that all installed packages have hashes listed, this allows +# dependency-requirements.txt to be used without requiring a hash for a +# (potentially unreleased) Certbot package. + letsencrypt==0.7.0 \ --hash=sha256:105a5fb107e45bcd0722eb89696986dcf5f08a86a321d6aef25a0c7c63375ade \ --hash=sha256:c36e532c486a7e92155ee09da54b436a3c420813ec1c590b98f635d924720de9 -# THE LINES BELOW ARE EDITED BY THE RELEASE SCRIPT; ADD ALL DEPENDENCIES ABOVE. - -acme==0.14.2 \ - --hash=sha256:b3068d360beccd3b23a81d7cd2522437d847328811b573a5fe14eb04147667cf \ - --hash=sha256:166b7f4858f5b144b03236b995b787a9da1e410121fb7dcac9c7d3b594bc6fcd -certbot==0.14.2 \ - --hash=sha256:525e15e43c833db9a9934308d69dcdd220fa799488cd84543748671c68aba73d \ - --hash=sha256:5bc8547dcfc0fc587e15253e264f79d8397e48bfbc8697d5aca87eae978769ac -certbot-apache==0.14.2 \ - --hash=sha256:15647d424a5a7e4c44c684324ac07a457a2e0d61fce1acaa421c0b641941a350 \ - --hash=sha256:e5220d3e6ee5114b41b398110dfbd8f13bd1e8c7902758634449e0b4ae515b76 -certbot-nginx==0.14.2 \ - --hash=sha256:231377fbdfb6303adddc73fe3f856b9fb6d0175db825650e39fe3dfd6a58f8ef \ - --hash=sha256:529a18280acf41f5f7e0fe7d82c0a5d4d197d14f82742eaec54bb1d3f69c325a +certbot==0.16.0 \ + --hash=sha256:e1cc2479f4f149a7128b67192c3f5f6c111b6b9ddcac421ebee8ac5030d5b09b \ + --hash=sha256:795801fd6b06b32060e364ac045312e6b26c6272d5ca32878277e5a2afdee186 +acme==0.16.0 \ + --hash=sha256:31592a744f7a6e7cbb4c5daf2deebc9af6b03997d6f80e5becc203ab8694edcf \ + --hash=sha256:538b69134cc50bdcdcc081b844c85158509022c61d7abc1f44216beeb95de9cd +certbot-apache==0.16.0 \ + --hash=sha256:337f84f391c7d7c31d84376e7a8c882ac119112a4aabceadd1a7de6a2f01ca06 \ + --hash=sha256:f1f37b56e1ceb0a28ccf51d54ca34444e6a708448845ef25372e223b9a82c4d4 +certbot-nginx==0.16.0 \ + --hash=sha256:1d57428202f8e7abdd99c3ddbcf374aad5f69c4262fe8dae53d2016e4ae04ded \ + --hash=sha256:77711655514d707ddf728195f00b2f6cdd1ad9db52440276b4a67664479c6954 UNLIKELY_EOF # ------------------------------------------------------------------------- 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 index 4ca5cc977..2f06add17 100644 Binary files a/certbot-compatibility-test/certbot_compatibility_test/testdata/nginx.tar.gz and b/certbot-compatibility-test/certbot_compatibility_test/testdata/nginx.tar.gz differ 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 c5ec54e22..071b97d22 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.15.0.dev0' +version = '0.17.0.dev0' install_requires = [ 'certbot', diff --git a/certbot-dns-cloudflare/certbot_dns_cloudflare/__init__.py b/certbot-dns-cloudflare/certbot_dns_cloudflare/__init__.py index f4820e1ca..7e53f83ce 100644 --- a/certbot-dns-cloudflare/certbot_dns_cloudflare/__init__.py +++ b/certbot-dns-cloudflare/certbot_dns_cloudflare/__init__.py @@ -1 +1,86 @@ -"""Cloudflare DNS Authenticator""" +""" +The `~certbot_dns_cloudflare.dns_cloudflare` plugin automates the process of +completing a ``dns-01`` challenge (`~acme.challenges.DNS01`) by creating, and +subsequently removing, TXT records using the Cloudflare API. + + +Named Arguments +--------------- + +======================================== ===================================== +``--dns-cloudflare-credentials`` Cloudflare credentials_ INI file. + (Required) +``--dns-cloudflare-propagation-seconds`` The number of seconds to wait for DNS + to propagate before asking the ACME + server to verify the DNS record. + (Default: 10) +======================================== ===================================== + + +Credentials +----------- + +Use of this plugin requires a configuration file containing Cloudflare API +credentials, obtained from your Cloudflare +`account page `_. + +.. code-block:: ini + :name: credentials.ini + :caption: Example credentials file: + + # Cloudflare API credentials used by Certbot + dns_cloudflare_email = cloudflare@example.com + dns_cloudflare_api_key = 0123456789abcdef0123456789abcdef01234567 + +The path to this file can be provided interactively or using the +``--dns-cloudflare-credentials`` command-line argument. Certbot records the path +to this file for use during renewal, but does not store the file's contents. + +.. caution:: + You should protect these API credentials as you would the password to your + Cloudflare account. Users who can read this file can use these credentials + to issue arbitrary API calls on your behalf. Users who can cause Certbot to + run using these credentials can complete a ``dns-01`` challenge to acquire + new certificates or revoke existing certificates for associated domains, + even if those domains aren't being managed by this server. + +Certbot will emit a warning if it detects that the credentials file can be +accessed by other users on your system. The warning reads "Unsafe permissions +on credentials configuration file", followed by the path to the credentials +file. This warning will be emitted each time Certbot uses the credentials file, +including for renewal, and cannot be silenced except by addressing the issue +(e.g., by using a command like ``chmod 600`` to restrict access to the file). + + +Examples +-------- + +.. code-block:: bash + :caption: To acquire a certificate for ``example.com`` + + certbot certonly \\ + --dns-cloudflare \\ + --dns-cloudflare-credentials ~/.secrets/certbot/cloudflare.ini \\ + -d example.com + +.. code-block:: bash + :caption: To acquire a single certificate for both ``example.com`` and + ``www.example.com`` + + certbot certonly \\ + --dns-cloudflare \\ + --dns-cloudflare-credentials ~/.secrets/certbot/cloudflare.ini \\ + -d example.com \\ + -d www.example.com + +.. code-block:: bash + :caption: To acquire a certificate for ``example.com``, waiting 60 seconds + for DNS propagation + + certbot certonly \\ + --dns-cloudflare \\ + --dns-cloudflare-credentials ~/.secrets/certbot/cloudflare.ini \\ + --dns-cloudflare-propagation-seconds 60 \\ + -d example.com + +""" diff --git a/certbot-dns-cloudflare/certbot_dns_cloudflare/dns_cloudflare.py b/certbot-dns-cloudflare/certbot_dns_cloudflare/dns_cloudflare.py index 6979581ee..f1156642f 100644 --- a/certbot-dns-cloudflare/certbot_dns_cloudflare/dns_cloudflare.py +++ b/certbot-dns-cloudflare/certbot_dns_cloudflare/dns_cloudflare.py @@ -21,7 +21,8 @@ class Authenticator(dns_common.DNSAuthenticator): This Authenticator uses the Cloudflare API to fulfill a dns-01 challenge. """ - description = 'Obtain certs using a DNS TXT record (if you are using Cloudflare for DNS).' + description = ('Obtain certificates using a DNS TXT record (if you are using Cloudflare for ' + 'DNS).') ttl = 120 def __init__(self, *args, **kwargs): diff --git a/certbot-dns-cloudflare/docs/api/dns_cloudflare.rst b/certbot-dns-cloudflare/docs/api/dns_cloudflare.rst index 939d4c0b4..35f525201 100644 --- a/certbot-dns-cloudflare/docs/api/dns_cloudflare.rst +++ b/certbot-dns-cloudflare/docs/api/dns_cloudflare.rst @@ -1,5 +1,5 @@ :mod:`certbot_dns_cloudflare.dns_cloudflare` --------------------------------------- +-------------------------------------------- .. automodule:: certbot_dns_cloudflare.dns_cloudflare :members: diff --git a/certbot-dns-cloudflare/setup.py b/certbot-dns-cloudflare/setup.py index 26570a0ff..7162a1dcf 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.15.0.dev0' +version = '0.17.0.dev0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot-dns-cloudxns/certbot_dns_cloudxns/__init__.py b/certbot-dns-cloudxns/certbot_dns_cloudxns/__init__.py index 8df02d0fa..6ddbdfe5a 100644 --- a/certbot-dns-cloudxns/certbot_dns_cloudxns/__init__.py +++ b/certbot-dns-cloudxns/certbot_dns_cloudxns/__init__.py @@ -1 +1,86 @@ -"""CloudXNS DNS Authenticator""" +""" +The `~certbot_dns_cloudxns.dns_cloudxns` plugin automates the process of +completing a ``dns-01`` challenge (`~acme.challenges.DNS01`) by creating, and +subsequently removing, TXT records using the CloudXNS API. + + +Named Arguments +--------------- + +======================================== ===================================== +``--dns-cloudxns-credentials`` CloudXNS credentials_ INI file. + (Required) +``--dns-cloudxns-propagation-seconds`` The number of seconds to wait for DNS + to propagate before asking the ACME + server to verify the DNS record. + (Default: 30) +======================================== ===================================== + + +Credentials +----------- + +Use of this plugin requires a configuration file containing CloudXNS API +credentials, obtained from your CloudXNS +`API page `_. + +.. code-block:: ini + :name: credentials.ini + :caption: Example credentials file: + + # CloudXNS API credentials used by Certbot + dns_cloudxns_api_key = 1234567890abcdef1234567890abcdef + dns_cloudxns_secret_key = 1122334455667788 + +The path to this file can be provided interactively or using the +``--dns-cloudxns-credentials`` command-line argument. Certbot records the path +to this file for use during renewal, but does not store the file's contents. + +.. caution:: + You should protect these API credentials as you would the password to your + CloudXNS account. Users who can read this file can use these credentials to + issue arbitrary API calls on your behalf. Users who can cause Certbot to run + using these credentials can complete a ``dns-01`` challenge to acquire new + certificates or revoke existing certificates for associated domains, even if + those domains aren't being managed by this server. + +Certbot will emit a warning if it detects that the credentials file can be +accessed by other users on your system. The warning reads "Unsafe permissions +on credentials configuration file", followed by the path to the credentials +file. This warning will be emitted each time Certbot uses the credentials file, +including for renewal, and cannot be silenced except by addressing the issue +(e.g., by using a command like ``chmod 600`` to restrict access to the file). + + +Examples +-------- + +.. code-block:: bash + :caption: To acquire a certificate for ``example.com`` + + certbot certonly \\ + --dns-cloudxns \\ + --dns-cloudxns-credentials ~/.secrets/certbot/cloudxns.ini \\ + -d example.com + +.. code-block:: bash + :caption: To acquire a single certificate for both ``example.com`` and + ``www.example.com`` + + certbot certonly \\ + --dns-cloudxns \\ + --dns-cloudxns-credentials ~/.secrets/certbot/cloudxns.ini \\ + -d example.com \\ + -d www.example.com + +.. code-block:: bash + :caption: To acquire a certificate for ``example.com``, waiting 60 seconds + for DNS propagation + + certbot certonly \\ + --dns-cloudxns \\ + --dns-cloudxns-credentials ~/.secrets/certbot/cloudxns.ini \\ + --dns-cloudxns-propagation-seconds 60 \\ + -d example.com + +""" diff --git a/certbot-dns-cloudxns/certbot_dns_cloudxns/dns_cloudxns.py b/certbot-dns-cloudxns/certbot_dns_cloudxns/dns_cloudxns.py index 2e9d23a88..674194fee 100644 --- a/certbot-dns-cloudxns/certbot_dns_cloudxns/dns_cloudxns.py +++ b/certbot-dns-cloudxns/certbot_dns_cloudxns/dns_cloudxns.py @@ -22,7 +22,7 @@ class Authenticator(dns_common.DNSAuthenticator): This Authenticator uses the CloudXNS DNS API to fulfill a dns-01 challenge. """ - description = 'Obtain certs using a DNS TXT record (if you are using CloudXNS for DNS).' + description = 'Obtain certificates using a DNS TXT record (if you are using CloudXNS for DNS).' ttl = 60 def __init__(self, *args, **kwargs): diff --git a/certbot-dns-cloudxns/setup.py b/certbot-dns-cloudxns/setup.py index 4849007bc..024e32918 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.15.0.dev0' +version = '0.17.0.dev0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot-dns-digitalocean/certbot_dns_digitalocean/__init__.py b/certbot-dns-digitalocean/certbot_dns_digitalocean/__init__.py index 40b2527f8..3ab8df041 100644 --- a/certbot-dns-digitalocean/certbot_dns_digitalocean/__init__.py +++ b/certbot-dns-digitalocean/certbot_dns_digitalocean/__init__.py @@ -1 +1,86 @@ -"""DigitalOcean DNS Authenticator""" +""" +The `~certbot_dns_digitalocean.dns_digitalocean` plugin automates the process of +completing a ``dns-01`` challenge (`~acme.challenges.DNS01`) by creating, and +subsequently removing, TXT records using the DigitalOcean API. + + +Named Arguments +--------------- + +========================================== =================================== +``--dns-digitalocean-credentials`` DigitalOcean credentials_ INI file. + (Required) +``--dns-digitalocean-propagation-seconds`` The number of seconds to wait for + DNS to propagate before asking the + ACME server to verify the DNS + record. + (Default: 10) +========================================== =================================== + + +Credentials +----------- + +Use of this plugin requires a configuration file containing DigitalOcean API +credentials, obtained from your DigitalOcean account's `Applications & API +Tokens page `_. + +.. code-block:: ini + :name: credentials.ini + :caption: Example credentials file: + + # DigitalOcean API credentials used by Certbot + dns_digitalocean_token = 0000111122223333444455556666777788889999aaaabbbbccccddddeeeeffff + +The path to this file can be provided interactively or using the +``--dns-digitalocean-credentials`` command-line argument. Certbot records the +path to this file for use during renewal, but does not store the file's contents. + +.. caution:: + You should protect these API credentials as you would the password to your + DigitalOcean account. Users who can read this file can use these credentials + to issue arbitrary API calls on your behalf. Users who can cause Certbot to + run using these credentials can complete a ``dns-01`` challenge to acquire + new certificates or revoke existing certificates for associated domains, + even if those domains aren't being managed by this server. + +Certbot will emit a warning if it detects that the credentials file can be +accessed by other users on your system. The warning reads "Unsafe permissions +on credentials configuration file", followed by the path to the credentials +file. This warning will be emitted each time Certbot uses the credentials file, +including for renewal, and cannot be silenced except by addressing the issue +(e.g., by using a command like ``chmod 600`` to restrict access to the file). + + +Examples +-------- + +.. code-block:: bash + :caption: To acquire a certificate for ``example.com`` + + certbot certonly \\ + --dns-digitalocean \\ + --dns-digitalocean-credentials ~/.secrets/certbot/digitalocean.ini \\ + -d example.com + +.. code-block:: bash + :caption: To acquire a single certificate for both ``example.com`` and + ``www.example.com`` + + certbot certonly \\ + --dns-digitalocean \\ + --dns-digitalocean-credentials ~/.secrets/certbot/digitalocean.ini \\ + -d example.com \\ + -d www.example.com + +.. code-block:: bash + :caption: To acquire a certificate for ``example.com``, waiting 60 seconds + for DNS propagation + + certbot certonly \\ + --dns-digitalocean \\ + --dns-digitalocean-credentials ~/.secrets/certbot/digitalocean.ini \\ + --dns-digitalocean-propagation-seconds 60 \\ + -d example.com + +""" diff --git a/certbot-dns-digitalocean/certbot_dns_digitalocean/dns_digitalocean_test.py b/certbot-dns-digitalocean/certbot_dns_digitalocean/dns_digitalocean_test.py index 7e97eed07..11c8c57d5 100644 --- a/certbot-dns-digitalocean/certbot_dns_digitalocean/dns_digitalocean_test.py +++ b/certbot-dns-digitalocean/certbot_dns_digitalocean/dns_digitalocean_test.py @@ -5,6 +5,7 @@ import unittest import digitalocean import mock +import six from certbot import errors from certbot.plugins import dns_test_common @@ -133,8 +134,8 @@ class DigitalOceanClientTest(unittest.TestCase): correct_record_mock.destroy.assert_called() - self.assertItemsEqual(first_record_mock.destroy.call_args_list, []) - self.assertItemsEqual(last_record_mock.destroy.call_args_list, []) + six.assertCountEqual(self, first_record_mock.destroy.call_args_list, []) + six.assertCountEqual(self, last_record_mock.destroy.call_args_list, []) def test_del_txt_record_error_finding_domain(self): self.manager.get_all_domains.side_effect = API_ERROR diff --git a/certbot-dns-digitalocean/setup.py b/certbot-dns-digitalocean/setup.py index f76eaa7db..dc09fdd0b 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.15.0.dev0' +version = '0.17.0.dev0' # Please update tox.ini when modifying dependency version requirements install_requires = [ @@ -15,6 +15,7 @@ install_requires = [ # For pkg_resources. >=1.0 so pip resolves it to a version cryptography # will tolerate; see #2599: 'setuptools>=1.0', + 'six', 'zope.interface', ] diff --git a/certbot-dns-dnsimple/certbot_dns_dnsimple/__init__.py b/certbot-dns-dnsimple/certbot_dns_dnsimple/__init__.py index 1d6747249..f8a2e83aa 100644 --- a/certbot-dns-dnsimple/certbot_dns_dnsimple/__init__.py +++ b/certbot-dns-dnsimple/certbot_dns_dnsimple/__init__.py @@ -1 +1,85 @@ -"""DNSimple DNS Authenticator""" +""" +The `~certbot_dns_dnsimple.dns_dnsimple` plugin automates the process of +completing a ``dns-01`` challenge (`~acme.challenges.DNS01`) by creating, and +subsequently removing, TXT records using the DNSimple API. + + +Named Arguments +--------------- + +======================================== ===================================== +``--dns-dnsimple-credentials`` DNSimple credentials_ INI file. + (Required) +``--dns-dnsimple-propagation-seconds`` The number of seconds to wait for DNS + to propagate before asking the ACME + server to verify the DNS record. + (Default: 30) +======================================== ===================================== + + +Credentials +----------- + +Use of this plugin requires a configuration file containing DNSimple API +credentials, obtained from your DNSimple +`account page `_. + +.. code-block:: ini + :name: credentials.ini + :caption: Example credentials file: + + # DNSimple API credentials used by Certbot + dns_dnsimple_token = MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAw + +The path to this file can be provided interactively or using the +``--dns-dnsimple-credentials`` command-line argument. Certbot records the path +to this file for use during renewal, but does not store the file's contents. + +.. caution:: + You should protect these API credentials as you would the password to your + DNSimple account. Users who can read this file can use these credentials + to issue arbitrary API calls on your behalf. Users who can cause Certbot to + run using these credentials can complete a ``dns-01`` challenge to acquire + new certificates or revoke existing certificates for associated domains, + even if those domains aren't being managed by this server. + +Certbot will emit a warning if it detects that the credentials file can be +accessed by other users on your system. The warning reads "Unsafe permissions +on credentials configuration file", followed by the path to the credentials +file. This warning will be emitted each time Certbot uses the credentials file, +including for renewal, and cannot be silenced except by addressing the issue +(e.g., by using a command like ``chmod 600`` to restrict access to the file). + + +Examples +-------- + +.. code-block:: bash + :caption: To acquire a certificate for ``example.com`` + + certbot certonly \\ + --dns-dnsimple \\ + --dns-dnsimple-credentials ~/.secrets/certbot/dnsimple.ini \\ + -d example.com + +.. code-block:: bash + :caption: To acquire a single certificate for both ``example.com`` and + ``www.example.com`` + + certbot certonly \\ + --dns-dnsimple \\ + --dns-dnsimple-credentials ~/.secrets/certbot/dnsimple.ini \\ + -d example.com \\ + -d www.example.com + +.. code-block:: bash + :caption: To acquire a certificate for ``example.com``, waiting 60 seconds + for DNS propagation + + certbot certonly \\ + --dns-dnsimple \\ + --dns-dnsimple-credentials ~/.secrets/certbot/dnsimple.ini \\ + --dns-dnsimple-propagation-seconds 60 \\ + -d example.com + +""" diff --git a/certbot-dns-dnsimple/certbot_dns_dnsimple/dns_dnsimple.py b/certbot-dns-dnsimple/certbot_dns_dnsimple/dns_dnsimple.py index f489f889a..f3a98567e 100644 --- a/certbot-dns-dnsimple/certbot_dns_dnsimple/dns_dnsimple.py +++ b/certbot-dns-dnsimple/certbot_dns_dnsimple/dns_dnsimple.py @@ -22,7 +22,7 @@ class Authenticator(dns_common.DNSAuthenticator): This Authenticator uses the DNSimple v2 API to fulfill a dns-01 challenge. """ - description = 'Obtain certs using a DNS TXT record (if you are using DNSimple for DNS).' + description = 'Obtain certificates using a DNS TXT record (if you are using DNSimple for DNS).' ttl = 60 def __init__(self, *args, **kwargs): diff --git a/certbot-dns-dnsimple/setup.py b/certbot-dns-dnsimple/setup.py index bfa89ea25..98d3e0a25 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.15.0.dev0' +version = '0.17.0.dev0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot-dns-dnsmadeeasy/LICENSE.txt b/certbot-dns-dnsmadeeasy/LICENSE.txt new file mode 100644 index 000000000..981c46c9f --- /dev/null +++ b/certbot-dns-dnsmadeeasy/LICENSE.txt @@ -0,0 +1,190 @@ + Copyright 2015 Electronic Frontier Foundation and others + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/certbot-dns-dnsmadeeasy/MANIFEST.in b/certbot-dns-dnsmadeeasy/MANIFEST.in new file mode 100644 index 000000000..18f018c08 --- /dev/null +++ b/certbot-dns-dnsmadeeasy/MANIFEST.in @@ -0,0 +1,3 @@ +include LICENSE.txt +include README.rst +recursive-include docs * diff --git a/certbot-dns-dnsmadeeasy/README.rst b/certbot-dns-dnsmadeeasy/README.rst new file mode 100644 index 000000000..4456914ce --- /dev/null +++ b/certbot-dns-dnsmadeeasy/README.rst @@ -0,0 +1 @@ +DNS Made Easy DNS Authenticator plugin for Certbot diff --git a/certbot-dns-dnsmadeeasy/certbot_dns_dnsmadeeasy/__init__.py b/certbot-dns-dnsmadeeasy/certbot_dns_dnsmadeeasy/__init__.py new file mode 100644 index 000000000..52f055237 --- /dev/null +++ b/certbot-dns-dnsmadeeasy/certbot_dns_dnsmadeeasy/__init__.py @@ -0,0 +1,86 @@ +""" +The `~certbot_dns_dnsmadeeasy.dns_dnsmadeeasy` plugin automates the process of +completing a ``dns-01`` challenge (`~acme.challenges.DNS01`) by creating, and +subsequently removing, TXT records using the DNS Made Easy API. + + +Named Arguments +--------------- + +========================================= ===================================== +``--dns-dnsmadeeasy-credentials`` DNS Made Easy credentials_ INI file. + (Required) +``--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) +========================================= ===================================== + + +Credentials +----------- + +Use of this plugin requires a configuration file containing DNS Made Easy API +credentials, obtained from your DNS Made Easy +`account page `_. + +.. code-block:: ini + :name: credentials.ini + :caption: Example credentials file: + + # DNS Made Easy API credentials used by Certbot + dns_dnsmadeeasy_api_key = 1c1a3c91-4770-4ce7-96f4-54c0eb0e457a + dns_dnsmadeeasy_secret_key = c9b5625f-9834-4ff8-baba-4ed5f32cae55 + +The path to this file can be provided interactively or using the +``--dns-dnsmadeeasy-credentials`` command-line argument. Certbot records the path +to this file for use during renewal, but does not store the file's contents. + +.. caution:: + You should protect these API credentials as you would the password to your + DNS Made Easy account. Users who can read this file can use these credentials + to issue arbitrary API calls on your behalf. Users who can cause Certbot to + run using these credentials can complete a ``dns-01`` challenge to acquire + new certificates or revoke existing certificates for associated domains, + even if those domains aren't being managed by this server. + +Certbot will emit a warning if it detects that the credentials file can be +accessed by other users on your system. The warning reads "Unsafe permissions +on credentials configuration file", followed by the path to the credentials +file. This warning will be emitted each time Certbot uses the credentials file, +including for renewal, and cannot be silenced except by addressing the issue +(e.g., by using a command like ``chmod 600`` to restrict access to the file). + + +Examples +-------- + +.. code-block:: bash + :caption: To acquire a certificate for ``example.com`` + + certbot certonly \\ + --dns-dnsmadeeasy \\ + --dns-dnsmadeeasy-credentials ~/.secrets/certbot/dnsmadeeasy.ini \\ + -d example.com + +.. code-block:: bash + :caption: To acquire a single certificate for both ``example.com`` and + ``www.example.com`` + + certbot certonly \\ + --dns-dnsmadeeasy \\ + --dns-dnsmadeeasy-credentials ~/.secrets/certbot/dnsmadeeasy.ini \\ + -d example.com \\ + -d www.example.com + +.. code-block:: bash + :caption: To acquire a certificate for ``example.com``, waiting 2 minutes + for DNS propagation + + certbot certonly \\ + --dns-dnsmadeeasy \\ + --dns-dnsmadeeasy-credentials ~/.secrets/certbot/dnsmadeeasy.ini \\ + --dns-dnsmadeeasy-propagation-seconds 120 \\ + -d example.com + +""" diff --git a/certbot-dns-dnsmadeeasy/certbot_dns_dnsmadeeasy/dns_dnsmadeeasy.py b/certbot-dns-dnsmadeeasy/certbot_dns_dnsmadeeasy/dns_dnsmadeeasy.py new file mode 100644 index 000000000..982edfdd3 --- /dev/null +++ b/certbot-dns-dnsmadeeasy/certbot_dns_dnsmadeeasy/dns_dnsmadeeasy.py @@ -0,0 +1,89 @@ +"""DNS Authenticator for DNS Made Easy DNS.""" +import logging + +import zope.interface +from lexicon.providers import dnsmadeeasy + +from certbot import errors +from certbot import interfaces +from certbot.plugins import dns_common +from certbot.plugins import dns_common_lexicon + +logger = logging.getLogger(__name__) + +ACCOUNT_URL = 'https://cp.dnsmadeeasy.com/account/info' + + +@zope.interface.implementer(interfaces.IAuthenticator) +@zope.interface.provider(interfaces.IPluginFactory) +class Authenticator(dns_common.DNSAuthenticator): + """DNS Authenticator for DNS Made Easy + + This Authenticator uses the DNS Made Easy API to fulfill a dns-01 challenge. + """ + + description = ('Obtain certificates using a DNS TXT record (if you are using DNS Made Easy for ' + 'DNS).') + ttl = 60 + + def __init__(self, *args, **kwargs): + super(Authenticator, self).__init__(*args, **kwargs) + self.credentials = None + + @classmethod + def add_parser_arguments(cls, add): # pylint: disable=arguments-differ + super(Authenticator, cls).add_parser_arguments(add, default_propagation_seconds=60) + add('credentials', help='DNS Made Easy credentials INI file.') + + def more_info(self): # pylint: disable=missing-docstring,no-self-use + return 'This plugin configures a DNS TXT record to respond to a dns-01 challenge using ' + \ + 'the DNS Made Easy API.' + + def _setup_credentials(self): + self.credentials = self._configure_credentials( + 'credentials', + 'DNS Made Easy credentials INI file', + { + 'api-key': 'API key for DNS Made Easy account, obtained from {0}' + .format(ACCOUNT_URL), + 'secret-key': 'Secret key for DNS Made Easy account, obtained from {0}' + .format(ACCOUNT_URL) + } + ) + + def _perform(self, domain, validation_name, validation): + self._get_dnsmadeeasy_client().add_txt_record(domain, validation_name, validation) + + def _cleanup(self, domain, validation_name, validation): + self._get_dnsmadeeasy_client().del_txt_record(domain, validation_name, validation) + + def _get_dnsmadeeasy_client(self): + return _DNSMadeEasyLexiconClient(self.credentials.conf('api-key'), + self.credentials.conf('secret-key'), + self.ttl) + + +class _DNSMadeEasyLexiconClient(dns_common_lexicon.LexiconClient): + """ + Encapsulates all communication with the DNS Made Easy via Lexicon. + """ + + def __init__(self, api_key, secret_key, ttl): + super(_DNSMadeEasyLexiconClient, self).__init__() + + self.provider = dnsmadeeasy.Provider({ + 'auth_username': api_key, + 'auth_token': secret_key, + 'ttl': ttl, + }) + + def _handle_http_error(self, e, domain_name): + if domain_name in str(e) and str(e).startswith('404 Client Error: Not Found for url:'): + return + + hint = None + if str(e).startswith('403 Client Error: Forbidden for url:'): + hint = 'Are your API key and Secret key values correct?' + + return errors.PluginError('Error determining zone identifier: {0}.{1}' + .format(e, ' ({0})'.format(hint) if hint else '')) diff --git a/certbot-dns-dnsmadeeasy/certbot_dns_dnsmadeeasy/dns_dnsmadeeasy_test.py b/certbot-dns-dnsmadeeasy/certbot_dns_dnsmadeeasy/dns_dnsmadeeasy_test.py new file mode 100644 index 000000000..44a777e1b --- /dev/null +++ b/certbot-dns-dnsmadeeasy/certbot_dns_dnsmadeeasy/dns_dnsmadeeasy_test.py @@ -0,0 +1,56 @@ +"""Tests for certbot_dns_dnsmadeeasy.dns_dnsmadeeasy.""" + +import os +import unittest + +import mock +from requests.exceptions import HTTPError + +from certbot.plugins import dns_test_common +from certbot.plugins import dns_test_common_lexicon +from certbot.plugins.dns_test_common import DOMAIN +from certbot.tests import util as test_util + +API_KEY = 'foo' +SECRET_KEY = 'bar' + + +class AuthenticatorTest(test_util.TempDirTestCase, + dns_test_common_lexicon.BaseLexiconAuthenticatorTest): + + def setUp(self): + super(AuthenticatorTest, self).setUp() + + from certbot_dns_dnsmadeeasy.dns_dnsmadeeasy import Authenticator + + path = os.path.join(self.tempdir, 'file.ini') + dns_test_common.write({"dnsmadeeasy_api_key": API_KEY, + "dnsmadeeasy_secret_key": SECRET_KEY}, + path) + + self.config = mock.MagicMock(dnsmadeeasy_credentials=path, + dnsmadeeasy_propagation_seconds=0) # don't wait during tests + + self.auth = Authenticator(self.config, "dnsmadeeasy") + + self.mock_client = mock.MagicMock() + # _get_dnsmadeeasy_client | pylint: disable=protected-access + self.auth._get_dnsmadeeasy_client = mock.MagicMock(return_value=self.mock_client) + + +class DNSMadeEasyLexiconClientTest(unittest.TestCase, + dns_test_common_lexicon.BaseLexiconClientTest): + DOMAIN_NOT_FOUND = HTTPError('404 Client Error: Not Found for url: {0}.'.format(DOMAIN)) + LOGIN_ERROR = HTTPError('403 Client Error: Forbidden for url: {0}.'.format(DOMAIN)) + + def setUp(self): + from certbot_dns_dnsmadeeasy.dns_dnsmadeeasy import _DNSMadeEasyLexiconClient + + self.client = _DNSMadeEasyLexiconClient(API_KEY, SECRET_KEY, 0) + + self.provider_mock = mock.MagicMock() + self.client.provider = self.provider_mock + + +if __name__ == "__main__": + unittest.main() # pragma: no cover diff --git a/certbot-dns-dnsmadeeasy/docs/.gitignore b/certbot-dns-dnsmadeeasy/docs/.gitignore new file mode 100644 index 000000000..ba65b13af --- /dev/null +++ b/certbot-dns-dnsmadeeasy/docs/.gitignore @@ -0,0 +1 @@ +/_build/ diff --git a/certbot-dns-dnsmadeeasy/docs/Makefile b/certbot-dns-dnsmadeeasy/docs/Makefile new file mode 100644 index 000000000..9b46118d1 --- /dev/null +++ b/certbot-dns-dnsmadeeasy/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +SPHINXPROJ = certbot-dns-dnsmadeeasy +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) \ No newline at end of file diff --git a/certbot-dns-dnsmadeeasy/docs/api.rst b/certbot-dns-dnsmadeeasy/docs/api.rst new file mode 100644 index 000000000..8668ec5d8 --- /dev/null +++ b/certbot-dns-dnsmadeeasy/docs/api.rst @@ -0,0 +1,8 @@ +================= +API Documentation +================= + +.. toctree:: + :glob: + + api/** diff --git a/certbot-dns-dnsmadeeasy/docs/api/dns_dnsmadeeasy.rst b/certbot-dns-dnsmadeeasy/docs/api/dns_dnsmadeeasy.rst new file mode 100644 index 000000000..81948a77f --- /dev/null +++ b/certbot-dns-dnsmadeeasy/docs/api/dns_dnsmadeeasy.rst @@ -0,0 +1,5 @@ +:mod:`certbot_dns_dnsmadeeasy.dns_dnsmadeeasy` +------------------------------------ + +.. automodule:: certbot_dns_dnsmadeeasy.dns_dnsmadeeasy + :members: diff --git a/certbot-dns-dnsmadeeasy/docs/conf.py b/certbot-dns-dnsmadeeasy/docs/conf.py new file mode 100644 index 000000000..7d26f9742 --- /dev/null +++ b/certbot-dns-dnsmadeeasy/docs/conf.py @@ -0,0 +1,180 @@ +# -*- coding: utf-8 -*- +# +# certbot-dns-dnsmadeeasy documentation build configuration file, created by +# sphinx-quickstart on Wed May 10 18:39:34 2017. +# +# This file is execfile()d with the current directory set to its +# containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +import os +# import sys +# sys.path.insert(0, os.path.abspath('.')) + + +# -- General configuration ------------------------------------------------ + +# If your documentation needs a minimal Sphinx version, state it here. +# +needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = ['sphinx.ext.autodoc', + 'sphinx.ext.intersphinx', + 'sphinx.ext.todo', + 'sphinx.ext.coverage', + 'sphinx.ext.viewcode'] + +autodoc_member_order = 'bysource' +autodoc_default_flags = ['show-inheritance', 'private-members'] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# +# source_suffix = ['.rst', '.md'] +source_suffix = '.rst' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = u'certbot-dns-dnsmadeeasy' +copyright = u'2017, Certbot Project' +author = u'Certbot Project' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = u'0' +# The full version, including alpha/beta/rc tags. +release = u'0' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = 'en' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This patterns also effect to html_static_path and html_extra_path +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] + +default_role = 'py:obj' + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# If true, `todo` and `todoList` produce output, else they produce nothing. +todo_include_todos = True + + +# -- Options for HTML output ---------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# + +# http://docs.readthedocs.org/en/latest/theme.html#how-do-i-use-this-locally-and-on-read-the-docs +# on_rtd is whether we are on readthedocs.org +on_rtd = os.environ.get('READTHEDOCS', None) == 'True' +if not on_rtd: # only import and set the theme if we're building docs locally + import sphinx_rtd_theme + html_theme = 'sphinx_rtd_theme' + html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] +# otherwise, readthedocs.org uses their theme by default, so no need to specify it + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +# +# html_theme_options = {} + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + + +# -- Options for HTMLHelp output ------------------------------------------ + +# Output file base name for HTML help builder. +htmlhelp_basename = 'certbot-dns-dnsmadeeasydoc' + + +# -- Options for LaTeX output --------------------------------------------- + +latex_elements = { + # The paper size ('letterpaper' or 'a4paper'). + # + # 'papersize': 'letterpaper', + + # The font size ('10pt', '11pt' or '12pt'). + # + # 'pointsize': '10pt', + + # Additional stuff for the LaTeX preamble. + # + # 'preamble': '', + + # Latex figure (float) alignment + # + # 'figure_align': 'htbp', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + (master_doc, 'certbot-dns-dnsmadeeasy.tex', u'certbot-dns-dnsmadeeasy Documentation', + u'Certbot Project', 'manual'), +] + + +# -- Options for manual page output --------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + (master_doc, 'certbot-dns-dnsmadeeasy', u'certbot-dns-dnsmadeeasy Documentation', + [author], 1) +] + + +# -- Options for Texinfo output ------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + (master_doc, 'certbot-dns-dnsmadeeasy', u'certbot-dns-dnsmadeeasy Documentation', + author, 'certbot-dns-dnsmadeeasy', 'One line description of project.', + 'Miscellaneous'), +] + + + + +# Example configuration for intersphinx: refer to the Python standard library. +intersphinx_mapping = { + 'python': ('https://docs.python.org/', None), + 'acme': ('https://acme-python.readthedocs.org/en/latest/', None), + 'certbot': ('https://certbot.eff.org/docs/', None), +} diff --git a/certbot-dns-dnsmadeeasy/docs/index.rst b/certbot-dns-dnsmadeeasy/docs/index.rst new file mode 100644 index 000000000..2e9aef36b --- /dev/null +++ b/certbot-dns-dnsmadeeasy/docs/index.rst @@ -0,0 +1,28 @@ +.. certbot-dns-dnsmadeeasy documentation master file, created by + sphinx-quickstart on Wed May 10 18:39:34 2017. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to certbot-dns-dnsmadeeasy's documentation! +=================================================== + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + +.. toctree:: + :maxdepth: 1 + + api + +.. automodule:: certbot_dns_dnsmadeeasy + :members: + + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/certbot-dns-dnsmadeeasy/docs/make.bat b/certbot-dns-dnsmadeeasy/docs/make.bat new file mode 100644 index 000000000..f204c8393 --- /dev/null +++ b/certbot-dns-dnsmadeeasy/docs/make.bat @@ -0,0 +1,36 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=. +set BUILDDIR=_build +set SPHINXPROJ=certbot-dns-dnsmadeeasy + +if "%1" == "" goto help + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% + +:end +popd diff --git a/certbot-dns-dnsmadeeasy/setup.cfg b/certbot-dns-dnsmadeeasy/setup.cfg new file mode 100644 index 000000000..2a9acf13d --- /dev/null +++ b/certbot-dns-dnsmadeeasy/setup.cfg @@ -0,0 +1,2 @@ +[bdist_wheel] +universal = 1 diff --git a/certbot-dns-dnsmadeeasy/setup.py b/certbot-dns-dnsmadeeasy/setup.py new file mode 100644 index 000000000..a3ab24684 --- /dev/null +++ b/certbot-dns-dnsmadeeasy/setup.py @@ -0,0 +1,68 @@ +import sys + +from setuptools import setup +from setuptools import find_packages + + +version = '0.17.0.dev0' + +# Please update tox.ini when modifying dependency version requirements +install_requires = [ + 'acme=={0}'.format(version), + 'certbot=={0}'.format(version), + 'dns-lexicon', + 'mock', + # For pkg_resources. >=1.0 so pip resolves it to a version cryptography + # will tolerate; see #2599: + 'setuptools>=1.0', + 'zope.interface', +] + +docs_extras = [ + 'Sphinx>=1.0', # autodoc_member_order = 'bysource', autodoc_default_flags + 'sphinx_rtd_theme', +] + +setup( + name='certbot-dns-dnsmadeeasy', + version=version, + description="DNS Made Easy DNS Authenticator plugin for Certbot", + url='https://github.com/certbot/certbot', + author="Certbot Project", + author_email='client-dev@letsencrypt.org', + license='Apache License 2.0', + classifiers=[ + 'Development Status :: 3 - Alpha', + 'Environment :: Plugins', + 'Intended Audience :: System Administrators', + 'License :: OSI Approved :: Apache Software License', + 'Operating System :: POSIX :: Linux', + 'Programming Language :: Python', + 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.3', + 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', + 'Topic :: Internet :: WWW/HTTP', + 'Topic :: Security', + 'Topic :: System :: Installation/Setup', + 'Topic :: System :: Networking', + 'Topic :: System :: Systems Administration', + 'Topic :: Utilities', + ], + + packages=find_packages(), + include_package_data=True, + install_requires=install_requires, + extras_require={ + 'docs': docs_extras, + }, + entry_points={ + 'certbot.plugins': [ + 'dns-dnsmadeeasy = certbot_dns_dnsmadeeasy.dns_dnsmadeeasy:Authenticator', + ], + }, + test_suite='certbot_dns_dnsmadeeasy', +) diff --git a/certbot-dns-google/certbot_dns_google/__init__.py b/certbot-dns-google/certbot_dns_google/__init__.py index 9e9096d83..26685206c 100644 --- a/certbot-dns-google/certbot_dns_google/__init__.py +++ b/certbot-dns-google/certbot_dns_google/__init__.py @@ -1 +1,98 @@ -"""Google Cloud DNS Authenticator""" +""" +The `~certbot_dns_google.dns_google` plugin automates the process of +completing a ``dns-01`` challenge (`~acme.challenges.DNS01`) by creating, and +subsequently removing, TXT records using the Google Cloud DNS API. + + +Named Arguments +--------------- + +======================================== ===================================== +``--dns-google-credentials`` Google Cloud Platform credentials_ + JSON file. + (Required) +``--dns-google-propagation-seconds`` The number of seconds to wait for DNS + to propagate before asking the ACME + server to verify the DNS record. + (Default: 60) +======================================== ===================================== + + +Credentials +----------- + +Use of this plugin requires a configuration file containing Google Cloud +Platform API credentials for an account with the following permissions: + +* ``dns.changes.create`` +* ``dns.changes.get`` +* ``dns.managedZones.list`` +* ``dns.resourceRecordSets.create`` +* ``dns.resourceRecordSets.delete`` + +Google provides instructions for `creating a service account `_ and +`information about the required permissions `_. + +.. code-block:: json + :name: credentials.json + :caption: Example credentials file: + + { + "type": "service_account", + ... + } + +The path to this file can be provided interactively or using the +``--dns-google-credentials`` command-line argument. Certbot records the path +to this file for use during renewal, but does not store the file's contents. + +.. caution:: + You should protect these API credentials as you would a password. Users who + can read this file can use these credentials to issue some types of API calls + on your behalf, limited by the permissions assigned to the account. Users who + can cause Certbot to run using these credentials can complete a ``dns-01`` + challenge to acquire new certificates or revoke existing certificates for + domains these credentials are authorized to manage. + +Certbot will emit a warning if it detects that the credentials file can be +accessed by other users on your system. The warning reads "Unsafe permissions +on credentials configuration file", followed by the path to the credentials +file. This warning will be emitted each time Certbot uses the credentials file, +including for renewal, and cannot be silenced except by addressing the issue +(e.g., by using a command like ``chmod 600`` to restrict access to the file). + + +Examples +-------- + +.. code-block:: bash + :caption: To acquire a certificate for ``example.com`` + + certbot certonly \\ + --dns-google \\ + --dns-google-credentials ~/.secrets/certbot/google.json \\ + -d example.com + +.. code-block:: bash + :caption: To acquire a single certificate for both ``example.com`` and + ``www.example.com`` + + certbot certonly \\ + --dns-google \\ + --dns-google-credentials ~/.secrets/certbot/google.json \\ + -d example.com \\ + -d www.example.com + +.. code-block:: bash + :caption: To acquire a certificate for ``example.com``, waiting 120 seconds + for DNS propagation + + certbot certonly \\ + --dns-google \\ + --dns-google-credentials ~/.secrets/certbot/google.ini \\ + --dns-google-propagation-seconds 120 \\ + -d example.com + +""" diff --git a/certbot-dns-google/certbot_dns_google/dns_google.py b/certbot-dns-google/certbot_dns_google/dns_google.py index 908c020e1..39811782e 100644 --- a/certbot-dns-google/certbot_dns_google/dns_google.py +++ b/certbot-dns-google/certbot_dns_google/dns_google.py @@ -25,7 +25,8 @@ class Authenticator(dns_common.DNSAuthenticator): This Authenticator uses the Google Cloud DNS API to fulfill a dns-01 challenge. """ - description = 'Obtain certs using a DNS TXT record (if you are using Google Cloud DNS for DNS).' + description = ('Obtain certificates using a DNS TXT record (if you are using Google Cloud DNS ' + 'for DNS).') ttl = 60 def __init__(self, *args, **kwargs): diff --git a/certbot-dns-google/certbot_dns_google/dns_google_test.py b/certbot-dns-google/certbot_dns_google/dns_google_test.py index eb41fa4ee..95e3347a1 100644 --- a/certbot-dns-google/certbot_dns_google/dns_google_test.py +++ b/certbot-dns-google/certbot_dns_google/dns_google_test.py @@ -76,7 +76,7 @@ class GoogleClientTest(unittest.TestCase): @mock.patch('oauth2client.service_account.ServiceAccountCredentials.from_json_keyfile_name') @mock.patch('certbot_dns_google.dns_google.open', - mock.mock_open(read_data='{"project_id": "' + PROJECT_ID + '"}')) + mock.mock_open(read_data='{"project_id": "' + PROJECT_ID + '"}'), create=True) def test_add_txt_record(self, unused_credential_mock): client, changes = self._setUp_client_with_mock([{'managedZones': [{'id': self.zone}]}]) @@ -101,7 +101,7 @@ class GoogleClientTest(unittest.TestCase): @mock.patch('oauth2client.service_account.ServiceAccountCredentials.from_json_keyfile_name') @mock.patch('certbot_dns_google.dns_google.open', - mock.mock_open(read_data='{"project_id": "' + PROJECT_ID + '"}')) + mock.mock_open(read_data='{"project_id": "' + PROJECT_ID + '"}'), create=True) def test_add_txt_record_and_poll(self, unused_credential_mock): client, changes = self._setUp_client_with_mock([{'managedZones': [{'id': self.zone}]}]) changes.create.return_value.execute.return_value = {'status': 'pending', 'id': self.change} @@ -119,7 +119,7 @@ class GoogleClientTest(unittest.TestCase): @mock.patch('oauth2client.service_account.ServiceAccountCredentials.from_json_keyfile_name') @mock.patch('certbot_dns_google.dns_google.open', - mock.mock_open(read_data='{"project_id": "' + PROJECT_ID + '"}')) + mock.mock_open(read_data='{"project_id": "' + PROJECT_ID + '"}'), create=True) def test_add_txt_record_error_during_zone_lookup(self, unused_credential_mock): client, unused_changes = self._setUp_client_with_mock(API_ERROR) @@ -128,7 +128,7 @@ class GoogleClientTest(unittest.TestCase): @mock.patch('oauth2client.service_account.ServiceAccountCredentials.from_json_keyfile_name') @mock.patch('certbot_dns_google.dns_google.open', - mock.mock_open(read_data='{"project_id": "' + PROJECT_ID + '"}')) + mock.mock_open(read_data='{"project_id": "' + PROJECT_ID + '"}'), create=True) def test_add_txt_record_zone_not_found(self, unused_credential_mock): client, unused_changes = self._setUp_client_with_mock([{'managedZones': []}, {'managedZones': []}]) @@ -138,7 +138,7 @@ class GoogleClientTest(unittest.TestCase): @mock.patch('oauth2client.service_account.ServiceAccountCredentials.from_json_keyfile_name') @mock.patch('certbot_dns_google.dns_google.open', - mock.mock_open(read_data='{"project_id": "' + PROJECT_ID + '"}')) + mock.mock_open(read_data='{"project_id": "' + PROJECT_ID + '"}'), create=True) def test_add_txt_record_error_during_add(self, unused_credential_mock): client, changes = self._setUp_client_with_mock([{'managedZones': [{'id': self.zone}]}]) changes.create.side_effect = API_ERROR @@ -148,7 +148,7 @@ class GoogleClientTest(unittest.TestCase): @mock.patch('oauth2client.service_account.ServiceAccountCredentials.from_json_keyfile_name') @mock.patch('certbot_dns_google.dns_google.open', - mock.mock_open(read_data='{"project_id": "' + PROJECT_ID + '"}')) + mock.mock_open(read_data='{"project_id": "' + PROJECT_ID + '"}'), create=True) def test_del_txt_record(self, unused_credential_mock): client, changes = self._setUp_client_with_mock([{'managedZones': [{'id': self.zone}]}]) @@ -173,7 +173,7 @@ class GoogleClientTest(unittest.TestCase): @mock.patch('oauth2client.service_account.ServiceAccountCredentials.from_json_keyfile_name') @mock.patch('certbot_dns_google.dns_google.open', - mock.mock_open(read_data='{"project_id": "' + PROJECT_ID + '"}')) + mock.mock_open(read_data='{"project_id": "' + PROJECT_ID + '"}'), create=True) def test_del_txt_record_error_during_zone_lookup(self, unused_credential_mock): client, unused_changes = self._setUp_client_with_mock(API_ERROR) @@ -181,7 +181,7 @@ class GoogleClientTest(unittest.TestCase): @mock.patch('oauth2client.service_account.ServiceAccountCredentials.from_json_keyfile_name') @mock.patch('certbot_dns_google.dns_google.open', - mock.mock_open(read_data='{"project_id": "' + PROJECT_ID + '"}')) + mock.mock_open(read_data='{"project_id": "' + PROJECT_ID + '"}'), create=True) def test_del_txt_record_zone_not_found(self, unused_credential_mock): client, unused_changes = self._setUp_client_with_mock([{'managedZones': []}, {'managedZones': []}]) @@ -190,7 +190,7 @@ class GoogleClientTest(unittest.TestCase): @mock.patch('oauth2client.service_account.ServiceAccountCredentials.from_json_keyfile_name') @mock.patch('certbot_dns_google.dns_google.open', - mock.mock_open(read_data='{"project_id": "' + PROJECT_ID + '"}')) + mock.mock_open(read_data='{"project_id": "' + PROJECT_ID + '"}'), create=True) def test_del_txt_record_error_during_delete(self, unused_credential_mock): client, changes = self._setUp_client_with_mock([{'managedZones': [{'id': self.zone}]}]) changes.create.side_effect = API_ERROR diff --git a/certbot-dns-google/docs/_ext/jsonlexer.py b/certbot-dns-google/docs/_ext/jsonlexer.py new file mode 100644 index 000000000..1ad004d2b --- /dev/null +++ b/certbot-dns-google/docs/_ext/jsonlexer.py @@ -0,0 +1,16 @@ +"""Copied from https://stackoverflow.com/a/16863232""" + +def setup(app): + # enable Pygments json lexer + try: + import pygments + if pygments.__version__ >= '1.5': + # use JSON lexer included in recent versions of Pygments + from pygments.lexers import JsonLexer + else: + # use JSON lexer from pygments-json if installed + from pygson.json_lexer import JSONLexer as JsonLexer + except ImportError: + pass # not fatal if we have old (or no) Pygments and no pygments-json + else: + app.add_lexer('json', JsonLexer()) diff --git a/certbot-dns-google/docs/conf.py b/certbot-dns-google/docs/conf.py index 4ff1af1d1..bbb343ee8 100644 --- a/certbot-dns-google/docs/conf.py +++ b/certbot-dns-google/docs/conf.py @@ -17,8 +17,8 @@ # documentation root, use os.path.abspath to make it absolute, like shown here. # import os -# import sys -# sys.path.insert(0, os.path.abspath('.')) +import sys +sys.path.insert(0, os.path.abspath('_ext')) # -- General configuration ------------------------------------------------ @@ -34,7 +34,8 @@ extensions = ['sphinx.ext.autodoc', 'sphinx.ext.intersphinx', 'sphinx.ext.todo', 'sphinx.ext.coverage', - 'sphinx.ext.viewcode'] + 'sphinx.ext.viewcode', + 'jsonlexer'] autodoc_member_order = 'bysource' autodoc_default_flags = ['show-inheritance', 'private-members'] diff --git a/certbot-dns-google/setup.py b/certbot-dns-google/setup.py index 043a9ded1..4b7d21f2e 100644 --- a/certbot-dns-google/setup.py +++ b/certbot-dns-google/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.15.0.dev0' +version = '0.17.0.dev0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot-dns-luadns/LICENSE.txt b/certbot-dns-luadns/LICENSE.txt new file mode 100644 index 000000000..981c46c9f --- /dev/null +++ b/certbot-dns-luadns/LICENSE.txt @@ -0,0 +1,190 @@ + Copyright 2015 Electronic Frontier Foundation and others + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/certbot-dns-luadns/MANIFEST.in b/certbot-dns-luadns/MANIFEST.in new file mode 100644 index 000000000..18f018c08 --- /dev/null +++ b/certbot-dns-luadns/MANIFEST.in @@ -0,0 +1,3 @@ +include LICENSE.txt +include README.rst +recursive-include docs * diff --git a/certbot-dns-luadns/README.rst b/certbot-dns-luadns/README.rst new file mode 100644 index 000000000..e50843545 --- /dev/null +++ b/certbot-dns-luadns/README.rst @@ -0,0 +1 @@ +LuaDNS Authenticator plugin for Certbot diff --git a/certbot-dns-luadns/certbot_dns_luadns/__init__.py b/certbot-dns-luadns/certbot_dns_luadns/__init__.py new file mode 100644 index 000000000..e8e86f77c --- /dev/null +++ b/certbot-dns-luadns/certbot_dns_luadns/__init__.py @@ -0,0 +1,86 @@ +""" +The `~certbot_dns_luadns.dns_luadns` plugin automates the process of +completing a ``dns-01`` challenge (`~acme.challenges.DNS01`) by creating, and +subsequently removing, TXT records using the LuaDNS API. + + +Named Arguments +--------------- + +========================================= ===================================== +``--dns-luadns-credentials`` LuaDNS credentials_ INI file. + (Required) +``--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) +========================================= ===================================== + + +Credentials +----------- + +Use of this plugin requires a configuration file containing LuaDNS API +credentials, obtained from your LuaDNS +`account settings page `_. + +.. code-block:: ini + :name: credentials.ini + :caption: Example credentials file: + + # LuaDNS API credentials used by Certbot + dns_luadns_email = user@example.com + dns_luadns_token = 0123456789abcdef0123456789abcdef + +The path to this file can be provided interactively or using the +``--dns-luadns-credentials`` command-line argument. Certbot records the path +to this file for use during renewal, but does not store the file's contents. + +.. caution:: + You should protect these API credentials as you would the password to your + LuaDNS account. Users who can read this file can use these credentials + to issue arbitrary API calls on your behalf. Users who can cause Certbot to + run using these credentials can complete a ``dns-01`` challenge to acquire + new certificates or revoke existing certificates for associated domains, + even if those domains aren't being managed by this server. + +Certbot will emit a warning if it detects that the credentials file can be +accessed by other users on your system. The warning reads "Unsafe permissions +on credentials configuration file", followed by the path to the credentials +file. This warning will be emitted each time Certbot uses the credentials file, +including for renewal, and cannot be silenced except by addressing the issue +(e.g., by using a command like ``chmod 600`` to restrict access to the file). + + +Examples +-------- + +.. code-block:: bash + :caption: To acquire a certificate for ``example.com`` + + certbot certonly \\ + --dns-luadns \\ + --dns-luadns-credentials ~/.secrets/certbot/luadns.ini \\ + -d example.com + +.. code-block:: bash + :caption: To acquire a single certificate for both ``example.com`` and + ``www.example.com`` + + certbot certonly \\ + --dns-luadns \\ + --dns-luadns-credentials ~/.secrets/certbot/luadns.ini \\ + -d example.com \\ + -d www.example.com + +.. code-block:: bash + :caption: To acquire a certificate for ``example.com``, waiting 2 minutes + for DNS propagation + + certbot certonly \\ + --dns-luadns \\ + --dns-luadns-credentials ~/.secrets/certbot/luadns.ini \\ + --dns-luadns-propagation-seconds 120 \\ + -d example.com + +""" diff --git a/certbot-dns-luadns/certbot_dns_luadns/dns_luadns.py b/certbot-dns-luadns/certbot_dns_luadns/dns_luadns.py new file mode 100644 index 000000000..00b62e6e1 --- /dev/null +++ b/certbot-dns-luadns/certbot_dns_luadns/dns_luadns.py @@ -0,0 +1,83 @@ +"""DNS Authenticator for LuaDNS DNS.""" +import logging + +import zope.interface +from lexicon.providers import luadns + +from certbot import errors +from certbot import interfaces +from certbot.plugins import dns_common +from certbot.plugins import dns_common_lexicon + +logger = logging.getLogger(__name__) + +ACCOUNT_URL = 'https://api.luadns.com/settings' + + +@zope.interface.implementer(interfaces.IAuthenticator) +@zope.interface.provider(interfaces.IPluginFactory) +class Authenticator(dns_common.DNSAuthenticator): + """DNS Authenticator for LuaDNS + + This Authenticator uses the LuaDNS API to fulfill a dns-01 challenge. + """ + + description = 'Obtain certificates using a DNS TXT record (if you are using LuaDNS for DNS).' + ttl = 60 + + def __init__(self, *args, **kwargs): + super(Authenticator, self).__init__(*args, **kwargs) + self.credentials = None + + @classmethod + def add_parser_arguments(cls, add): # pylint: disable=arguments-differ + super(Authenticator, cls).add_parser_arguments(add, default_propagation_seconds=30) + add('credentials', help='LuaDNS credentials INI file.') + + def more_info(self): # pylint: disable=missing-docstring,no-self-use + return 'This plugin configures a DNS TXT record to respond to a dns-01 challenge using ' + \ + 'the LuaDNS API.' + + def _setup_credentials(self): + self.credentials = self._configure_credentials( + 'credentials', + 'LuaDNS credentials INI file', + { + 'email': 'email address associated with LuaDNS account', + 'token': 'API token for LuaDNS account, obtained from {0}'.format(ACCOUNT_URL) + } + ) + + def _perform(self, domain, validation_name, validation): + self._get_luadns_client().add_txt_record(domain, validation_name, validation) + + def _cleanup(self, domain, validation_name, validation): + self._get_luadns_client().del_txt_record(domain, validation_name, validation) + + def _get_luadns_client(self): + return _LuaDNSLexiconClient(self.credentials.conf('email'), + self.credentials.conf('token'), + self.ttl) + + +class _LuaDNSLexiconClient(dns_common_lexicon.LexiconClient): + """ + Encapsulates all communication with the LuaDNS via Lexicon. + """ + + def __init__(self, email, token, ttl): + super(_LuaDNSLexiconClient, self).__init__() + + self.provider = luadns.Provider({ + 'auth_username': email, + 'auth_token': token, + 'ttl': ttl, + }) + + def _handle_http_error(self, e, domain_name): + hint = None + if str(e).startswith('401 Client Error: Unauthorized for url:'): + hint = 'Are your email and API token values correct?' + + return errors.PluginError('Error determining zone identifier for {0}: {1}.{2}' + .format(domain_name, e, ' ({0})'.format(hint) if hint else '')) diff --git a/certbot-dns-luadns/certbot_dns_luadns/dns_luadns_test.py b/certbot-dns-luadns/certbot_dns_luadns/dns_luadns_test.py new file mode 100644 index 000000000..bf77e03e4 --- /dev/null +++ b/certbot-dns-luadns/certbot_dns_luadns/dns_luadns_test.py @@ -0,0 +1,52 @@ +"""Tests for certbot_dns_luadns.dns_luadns.""" + +import os +import unittest + +import mock +from requests.exceptions import HTTPError + +from certbot.plugins import dns_test_common +from certbot.plugins import dns_test_common_lexicon +from certbot.tests import util as test_util + +EMAIL = 'fake@example.com' +TOKEN = 'foo' + + +class AuthenticatorTest(test_util.TempDirTestCase, + dns_test_common_lexicon.BaseLexiconAuthenticatorTest): + + def setUp(self): + super(AuthenticatorTest, self).setUp() + + from certbot_dns_luadns.dns_luadns import Authenticator + + path = os.path.join(self.tempdir, 'file.ini') + dns_test_common.write({"luadns_email": EMAIL, "luadns_token": TOKEN}, path) + + self.config = mock.MagicMock(luadns_credentials=path, + luadns_propagation_seconds=0) # don't wait during tests + + self.auth = Authenticator(self.config, "luadns") + + self.mock_client = mock.MagicMock() + # _get_luadns_client | pylint: disable=protected-access + self.auth._get_luadns_client = mock.MagicMock(return_value=self.mock_client) + + +class LuaDNSLexiconClientTest(unittest.TestCase, dns_test_common_lexicon.BaseLexiconClientTest): + + LOGIN_ERROR = HTTPError("401 Client Error: Unauthorized for url: ...") + + def setUp(self): + from certbot_dns_luadns.dns_luadns import _LuaDNSLexiconClient + + self.client = _LuaDNSLexiconClient(EMAIL, TOKEN, 0) + + self.provider_mock = mock.MagicMock() + self.client.provider = self.provider_mock + + +if __name__ == "__main__": + unittest.main() # pragma: no cover diff --git a/certbot-dns-luadns/docs/.gitignore b/certbot-dns-luadns/docs/.gitignore new file mode 100644 index 000000000..ba65b13af --- /dev/null +++ b/certbot-dns-luadns/docs/.gitignore @@ -0,0 +1 @@ +/_build/ diff --git a/certbot-dns-luadns/docs/Makefile b/certbot-dns-luadns/docs/Makefile new file mode 100644 index 000000000..2f65aeccc --- /dev/null +++ b/certbot-dns-luadns/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +SPHINXPROJ = certbot-dns-luadns +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/certbot-dns-luadns/docs/api.rst b/certbot-dns-luadns/docs/api.rst new file mode 100644 index 000000000..8668ec5d8 --- /dev/null +++ b/certbot-dns-luadns/docs/api.rst @@ -0,0 +1,8 @@ +================= +API Documentation +================= + +.. toctree:: + :glob: + + api/** diff --git a/certbot-dns-luadns/docs/api/dns_luadns.rst b/certbot-dns-luadns/docs/api/dns_luadns.rst new file mode 100644 index 000000000..9aecbaf05 --- /dev/null +++ b/certbot-dns-luadns/docs/api/dns_luadns.rst @@ -0,0 +1,5 @@ +:mod:`certbot_dns_luadns.dns_luadns` +---------------------------------- + +.. automodule:: certbot_dns_luadns.dns_luadns + :members: diff --git a/certbot-dns-luadns/docs/conf.py b/certbot-dns-luadns/docs/conf.py new file mode 100644 index 000000000..bd81d5a5f --- /dev/null +++ b/certbot-dns-luadns/docs/conf.py @@ -0,0 +1,180 @@ +# -*- coding: utf-8 -*- +# +# certbot-dns-luadns documentation build configuration file, created by +# sphinx-quickstart on Wed May 10 18:46:01 2017. +# +# This file is execfile()d with the current directory set to its +# containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +import os +# import sys +# sys.path.insert(0, os.path.abspath('.')) + + +# -- General configuration ------------------------------------------------ + +# If your documentation needs a minimal Sphinx version, state it here. +# +needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = ['sphinx.ext.autodoc', + 'sphinx.ext.intersphinx', + 'sphinx.ext.todo', + 'sphinx.ext.coverage', + 'sphinx.ext.viewcode'] + +autodoc_member_order = 'bysource' +autodoc_default_flags = ['show-inheritance', 'private-members'] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# +# source_suffix = ['.rst', '.md'] +source_suffix = '.rst' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = u'certbot-dns-luadns' +copyright = u'2017, Certbot Project' +author = u'Certbot Project' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = u'0' +# The full version, including alpha/beta/rc tags. +release = u'0' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = 'en' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This patterns also effect to html_static_path and html_extra_path +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] + +default_role = 'py:obj' + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# If true, `todo` and `todoList` produce output, else they produce nothing. +todo_include_todos = True + + +# -- Options for HTML output ---------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# + +# http://docs.readthedocs.org/en/latest/theme.html#how-do-i-use-this-locally-and-on-read-the-docs +# on_rtd is whether we are on readthedocs.org +on_rtd = os.environ.get('READTHEDOCS', None) == 'True' +if not on_rtd: # only import and set the theme if we're building docs locally + import sphinx_rtd_theme + html_theme = 'sphinx_rtd_theme' + html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] +# otherwise, readthedocs.org uses their theme by default, so no need to specify it + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +# +# html_theme_options = {} + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + + +# -- Options for HTMLHelp output ------------------------------------------ + +# Output file base name for HTML help builder. +htmlhelp_basename = 'certbot-dns-luadnsdoc' + + +# -- Options for LaTeX output --------------------------------------------- + +latex_elements = { + # The paper size ('letterpaper' or 'a4paper'). + # + # 'papersize': 'letterpaper', + + # The font size ('10pt', '11pt' or '12pt'). + # + # 'pointsize': '10pt', + + # Additional stuff for the LaTeX preamble. + # + # 'preamble': '', + + # Latex figure (float) alignment + # + # 'figure_align': 'htbp', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + (master_doc, 'certbot-dns-luadns.tex', u'certbot-dns-luadns Documentation', + u'Certbot Project', 'manual'), +] + + +# -- Options for manual page output --------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + (master_doc, 'certbot-dns-luadns', u'certbot-dns-luadns Documentation', + [author], 1) +] + + +# -- Options for Texinfo output ------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + (master_doc, 'certbot-dns-luadns', u'certbot-dns-luadns Documentation', + author, 'certbot-dns-luadns', 'One line description of project.', + 'Miscellaneous'), +] + + + + +# Example configuration for intersphinx: refer to the Python standard library. +intersphinx_mapping = { + 'python': ('https://docs.python.org/', None), + 'acme': ('https://acme-python.readthedocs.org/en/latest/', None), + 'certbot': ('https://certbot.eff.org/docs/', None), +} diff --git a/certbot-dns-luadns/docs/index.rst b/certbot-dns-luadns/docs/index.rst new file mode 100644 index 000000000..589e925c0 --- /dev/null +++ b/certbot-dns-luadns/docs/index.rst @@ -0,0 +1,28 @@ +.. certbot-dns-luadns documentation master file, created by + sphinx-quickstart on Wed May 10 18:46:01 2017. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to certbot-dns-luadns's documentation! +============================================== + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + +.. toctree:: + :maxdepth: 1 + + api + +.. automodule:: certbot_dns_luadns + :members: + + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/certbot-dns-luadns/docs/make.bat b/certbot-dns-luadns/docs/make.bat new file mode 100644 index 000000000..9cfe0400e --- /dev/null +++ b/certbot-dns-luadns/docs/make.bat @@ -0,0 +1,36 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=. +set BUILDDIR=_build +set SPHINXPROJ=certbot-dns-luadns + +if "%1" == "" goto help + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% + +:end +popd diff --git a/certbot-dns-luadns/setup.cfg b/certbot-dns-luadns/setup.cfg new file mode 100644 index 000000000..2a9acf13d --- /dev/null +++ b/certbot-dns-luadns/setup.cfg @@ -0,0 +1,2 @@ +[bdist_wheel] +universal = 1 diff --git a/certbot-dns-luadns/setup.py b/certbot-dns-luadns/setup.py new file mode 100644 index 000000000..be0774e13 --- /dev/null +++ b/certbot-dns-luadns/setup.py @@ -0,0 +1,68 @@ +import sys + +from setuptools import setup +from setuptools import find_packages + + +version = '0.17.0.dev0' + +# Please update tox.ini when modifying dependency version requirements +install_requires = [ + 'acme=={0}'.format(version), + 'certbot=={0}'.format(version), + 'dns-lexicon', + 'mock', + # For pkg_resources. >=1.0 so pip resolves it to a version cryptography + # will tolerate; see #2599: + 'setuptools>=1.0', + 'zope.interface', +] + +docs_extras = [ + 'Sphinx>=1.0', # autodoc_member_order = 'bysource', autodoc_default_flags + 'sphinx_rtd_theme', +] + +setup( + name='certbot-dns-luadns', + version=version, + description="LuaDNS Authenticator plugin for Certbot", + url='https://github.com/certbot/certbot', + author="Certbot Project", + author_email='client-dev@letsencrypt.org', + license='Apache License 2.0', + classifiers=[ + 'Development Status :: 3 - Alpha', + 'Environment :: Plugins', + 'Intended Audience :: System Administrators', + 'License :: OSI Approved :: Apache Software License', + 'Operating System :: POSIX :: Linux', + 'Programming Language :: Python', + 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.3', + 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', + 'Topic :: Internet :: WWW/HTTP', + 'Topic :: Security', + 'Topic :: System :: Installation/Setup', + 'Topic :: System :: Networking', + 'Topic :: System :: Systems Administration', + 'Topic :: Utilities', + ], + + packages=find_packages(), + include_package_data=True, + install_requires=install_requires, + extras_require={ + 'docs': docs_extras, + }, + entry_points={ + 'certbot.plugins': [ + 'dns-luadns = certbot_dns_luadns.dns_luadns:Authenticator', + ], + }, + test_suite='certbot_dns_luadns', +) diff --git a/certbot-dns-nsone/certbot_dns_nsone/__init__.py b/certbot-dns-nsone/certbot_dns_nsone/__init__.py index 8c061edf7..e59be74a7 100644 --- a/certbot-dns-nsone/certbot_dns_nsone/__init__.py +++ b/certbot-dns-nsone/certbot_dns_nsone/__init__.py @@ -1 +1,85 @@ -"""NS1 DNS Authenticator""" +""" +The `~certbot_dns_nsone.dns_nsone` plugin automates the process of completing +a ``dns-01`` challenge (`~acme.challenges.DNS01`) by creating, and subsequently +removing, TXT records using the NS1 API. + + +Named Arguments +--------------- + +======================================== ===================================== +``--dns-nsone-credentials`` NS1 credentials_ INI file. + (Required) +``--dns-nsone-propagation-seconds`` The number of seconds to wait for DNS + to propagate before asking the ACME + server to verify the DNS record. + (Default: 30) +======================================== ===================================== + + +Credentials +----------- + +Use of this plugin requires a configuration file containing NS1 API credentials, +obtained from your NS1 +`account page `_. + +.. code-block:: ini + :name: credentials.ini + :caption: Example credentials file: + + # NS1 API credentials used by Certbot + dns_nsone_api_key = MDAwMDAwMDAwMDAwMDAw + +The path to this file can be provided interactively or using the +``--dns-nsone-credentials`` command-line argument. Certbot records the path +to this file for use during renewal, but does not store the file's contents. + +.. caution:: + You should protect these API credentials as you would the password to your + NS1 account. Users who can read this file can use these credentials to issue + arbitrary API calls on your behalf. Users who can cause Certbot to run using + these credentials can complete a ``dns-01`` challenge to acquire new + certificates or revoke existing certificates for associated domains, even if + those domains aren't being managed by this server. + +Certbot will emit a warning if it detects that the credentials file can be +accessed by other users on your system. The warning reads "Unsafe permissions +on credentials configuration file", followed by the path to the credentials +file. This warning will be emitted each time Certbot uses the credentials file, +including for renewal, and cannot be silenced except by addressing the issue +(e.g., by using a command like ``chmod 600`` to restrict access to the file). + + +Examples +-------- + +.. code-block:: bash + :caption: To acquire a certificate for ``example.com`` + + certbot certonly \\ + --dns-nsone \\ + --dns-nsone-credentials ~/.secrets/certbot/nsone.ini \\ + -d example.com + +.. code-block:: bash + :caption: To acquire a single certificate for both ``example.com`` and + ``www.example.com`` + + certbot certonly \\ + --dns-nsone \\ + --dns-nsone-credentials ~/.secrets/certbot/nsone.ini \\ + -d example.com \\ + -d www.example.com + +.. code-block:: bash + :caption: To acquire a certificate for ``example.com``, waiting 60 seconds + for DNS propagation + + certbot certonly \\ + --dns-nsone \\ + --dns-nsone-credentials ~/.secrets/certbot/nsone.ini \\ + --dns-nsone-propagation-seconds 60 \\ + -d example.com + +""" diff --git a/certbot-dns-nsone/certbot_dns_nsone/dns_nsone.py b/certbot-dns-nsone/certbot_dns_nsone/dns_nsone.py index be60ff39d..28db126c1 100644 --- a/certbot-dns-nsone/certbot_dns_nsone/dns_nsone.py +++ b/certbot-dns-nsone/certbot_dns_nsone/dns_nsone.py @@ -22,7 +22,7 @@ class Authenticator(dns_common.DNSAuthenticator): This Authenticator uses the NS1 API to fulfill a dns-01 challenge. """ - description = 'Obtain certs using a DNS TXT record (if you are using NS1 for DNS).' + description = 'Obtain certificates using a DNS TXT record (if you are using NS1 for DNS).' ttl = 60 def __init__(self, *args, **kwargs): diff --git a/certbot-dns-nsone/setup.py b/certbot-dns-nsone/setup.py index 658961bd8..addb69b6d 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.15.0.dev0' +version = '0.17.0.dev0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot-dns-rfc2136/LICENSE.txt b/certbot-dns-rfc2136/LICENSE.txt new file mode 100644 index 000000000..981c46c9f --- /dev/null +++ b/certbot-dns-rfc2136/LICENSE.txt @@ -0,0 +1,190 @@ + Copyright 2015 Electronic Frontier Foundation and others + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/certbot-dns-rfc2136/MANIFEST.in b/certbot-dns-rfc2136/MANIFEST.in new file mode 100644 index 000000000..18f018c08 --- /dev/null +++ b/certbot-dns-rfc2136/MANIFEST.in @@ -0,0 +1,3 @@ +include LICENSE.txt +include README.rst +recursive-include docs * diff --git a/certbot-dns-rfc2136/README.rst b/certbot-dns-rfc2136/README.rst new file mode 100644 index 000000000..e8ac2b337 --- /dev/null +++ b/certbot-dns-rfc2136/README.rst @@ -0,0 +1 @@ +RFC 2136 DNS Authenticator plugin for Certbot diff --git a/certbot-dns-rfc2136/certbot_dns_rfc2136/__init__.py b/certbot-dns-rfc2136/certbot_dns_rfc2136/__init__.py new file mode 100644 index 000000000..0f97869e2 --- /dev/null +++ b/certbot-dns-rfc2136/certbot_dns_rfc2136/__init__.py @@ -0,0 +1,136 @@ +""" +The `~certbot_dns_rfc2136.dns_rfc2136` plugin automates the process of +completing a ``dns-01`` challenge (`~acme.challenges.DNS01`) by creating, and +subsequently removing, TXT records using RFC 2136 Dynamic Updates. + + +Named Arguments +--------------- + +===================================== ===================================== +``--dns-rfc2136-credentials`` RFC 2136 credentials_ INI file. + (Required) +``--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) +===================================== ===================================== + + +Credentials +----------- + +Use of this plugin requires a configuration file containing the target DNS +server that supports RFC 2136 Dynamic Updates, the name of the TSIG key, the +TSIG key secret itself and the algorithm used if it's different to HMAC-MD5. + +.. code-block:: ini + :name: credentials.ini + :caption: Example credentials file: + + # Target DNS server + dns_rfc2136_server = 192.0.2.1 + # TSIG key name + dns_rfc2136_name = keyname. + # TSIG key secret + dns_rfc2136_secret = 4q4wM/2I180UXoMyN4INVhJNi8V9BCV+jMw2mXgZw/CSuxUT8C7NKKFs \ +AmKd7ak51vWKgSl12ib86oQRPkpDjg== + # TSIG key algorithm + dns_rfc2136_algorithm = HMAC-SHA512 + +The path to this file can be provided interactively or using the +``--dns-rfc2136-credentials`` command-line argument. Certbot records the +path to this file for use during renewal, but does not store the file's contents. + +.. caution:: + You should protect this TSIG key material as it can be used to potentially + add, update, or delete any record in the target DNS server. Users who can + read this file can use these credentials to issue arbitrary API calls on + your behalf. Users who can cause Certbot to run using these credentials can + complete a ``dns-01`` challenge to acquire new certificates or revoke + existing certificates for associated domains, even if those domains aren't + being managed by this server. + +Certbot will emit a warning if it detects that the credentials file can be +accessed by other users on your system. The warning reads "Unsafe permissions +on credentials configuration file", followed by the path to the credentials +file. This warning will be emitted each time Certbot uses the credentials file, +including for renewal, and cannot be silenced except by addressing the issue +(e.g., by using a command like ``chmod 600`` to restrict access to the file). + +Sample BIND configuration +''''''''''''''''''''''''' + +Here's a sample BIND configuration for Certbot to use. You will need to +generate a new TSIG key, include it in the BIND configuration and grant it +permission to issue updates on the target DNS zone. + +.. code-block:: bash + :caption: Generate a new SHA512 TSIG key + + dnssec-keygen -a HMAC-SHA512 -b 512 -n HOST keyname. + +.. note:: + There are a few tools shipped with BIND that can all generate TSIG keys; + ``dnssec-keygen``, ``rndc-confgen``, and ``ddns-confgen``. Try and use the + most secure algorithm supported by your DNS server. + +.. code-block:: none + :caption: Sample BIND configuration + + key "keyname." { + algorithm hmac-sha512; + secret "4q4wM/2I180UXoMyN4INVhJNi8V9BCV+jMw2mXgZw/CSuxUT8C7NKKFs \ +AmKd7ak51vWKgSl12ib86oQRPkpDjg=="; + }; + + zone "example.com." IN { + type master; + file "named.example.com"; + update-policy { + grant keyname. name _acme-challenge.example.com. txt; + }; + }; + +.. note:: + This configuration limits the scope of the TSIG key to just be able to + add and remove TXT records for one specific host for the purpose of + completing the ``dns-01`` challenge. If your version of BIND doesn't + support the + `update-policy `_ + directive then you can use the less-secure + `allow-update `_ + directive instead. + +Examples +-------- + +.. code-block:: bash + :caption: To acquire a certificate for ``example.com`` + + certbot certonly \\ + --dns-rfc2136 \\ + --dns-rfc2136-credentials ~/.secrets/certbot/rfc2136.ini \\ + -d example.com + +.. code-block:: bash + :caption: To acquire a single certificate for both ``example.com`` and + ``www.example.com`` + + certbot certonly \\ + --dns-rfc2136 \\ + --dns-rfc2136-credentials ~/.secrets/certbot/rfc2136.ini \\ + -d example.com \\ + -d www.example.com + +.. code-block:: bash + :caption: To acquire a certificate for ``example.com``, waiting 30 seconds + for DNS propagation + + certbot certonly \\ + --dns-rfc2136 \\ + --dns-rfc2136-credentials ~/.secrets/certbot/rfc2136.ini \\ + --dns-rfc2136-propagation-seconds 30 \\ + -d example.com + +""" diff --git a/certbot-dns-rfc2136/certbot_dns_rfc2136/dns_rfc2136.py b/certbot-dns-rfc2136/certbot_dns_rfc2136/dns_rfc2136.py new file mode 100644 index 000000000..b4b71bb75 --- /dev/null +++ b/certbot-dns-rfc2136/certbot_dns_rfc2136/dns_rfc2136.py @@ -0,0 +1,221 @@ +"""DNS Authenticator using RFC 2136 Dynamic Updates.""" +import logging + +import dns.flags +import dns.message +import dns.name +import dns.query +import dns.rdataclass +import dns.rdatatype +import dns.tsig +import dns.tsigkeyring +import dns.update +import zope.interface + +from certbot import errors +from certbot import interfaces +from certbot.plugins import dns_common + +logger = logging.getLogger(__name__) + + +@zope.interface.implementer(interfaces.IAuthenticator) +@zope.interface.provider(interfaces.IPluginFactory) +class Authenticator(dns_common.DNSAuthenticator): + """DNS Authenticator using RFC 2136 Dynamic Updates + + This Authenticator uses RFC 2136 Dynamic Updates to fulfull a dns-01 challenge. + """ + + ALGORITHMS = { + 'HMAC-MD5': dns.tsig.HMAC_MD5, + 'HMAC-SHA1': dns.tsig.HMAC_SHA1, + 'HMAC-SHA224': dns.tsig.HMAC_SHA224, + 'HMAC-SHA256': dns.tsig.HMAC_SHA256, + 'HMAC-SHA384': dns.tsig.HMAC_SHA384, + 'HMAC-SHA512': dns.tsig.HMAC_SHA512 + } + + description = 'Obtain certificates using a DNS TXT record (if you are using BIND for DNS).' + ttl = 120 + + def __init__(self, *args, **kwargs): + super(Authenticator, self).__init__(*args, **kwargs) + self.credentials = None + + @classmethod + def add_parser_arguments(cls, add): # pylint: disable=arguments-differ + super(Authenticator, cls).add_parser_arguments(add, default_propagation_seconds=60) + add('credentials', help='RFC 2136 credentials INI file.') + + def more_info(self): # pylint: disable=missing-docstring,no-self-use + return 'This plugin configures a DNS TXT record to respond to a dns-01 challenge using ' + \ + 'RFC 2136 Dynamic Updates.' + + def _validate_algorithm(self, credentials): + algorithm = credentials.conf('algorithm') + if algorithm: + if not self.ALGORITHMS.get(algorithm): + raise errors.PluginError("Unknown algorithm: {0}.".format(algorithm)) + + def _setup_credentials(self): + self.credentials = self._configure_credentials( + 'credentials', + 'RFC 2136 credentials INI file', + { + 'name': 'TSIG key name', + 'secret': 'TSIG key secret', + 'server': 'The target DNS server' + }, + self._validate_algorithm + ) + + def _perform(self, domain, validation_name, validation): + self._get_rfc2136_client().add_txt_record(domain, validation_name, validation, self.ttl) + + def _cleanup(self, domain, validation_name, validation): + self._get_rfc2136_client().del_txt_record(domain, validation_name, validation) + + def _get_rfc2136_client(self): + return _RFC2136Client(self.credentials.conf('server'), + self.credentials.conf('name'), + self.credentials.conf('secret'), + self.ALGORITHMS.get(self.credentials.conf('algorithm'), + dns.tsig.HMAC_MD5)) + + +class _RFC2136Client(object): + """ + Encapsulates all communication with the target DNS server. + """ + def __init__(self, server, key_name, key_secret, key_algorithm): + self.server = server + self.keyring = dns.tsigkeyring.from_text({ + key_name: key_secret + }) + self.algorithm = key_algorithm + + def add_txt_record(self, domain_name, record_name, record_content, record_ttl): + """ + Add a TXT record using the supplied information. + + :param str domain: The domain to use to find the closest SOA. + :param str record_name: The record name (typically beginning with '_acme-challenge.'). + :param str record_content: The record content (typically the challenge validation). + :param int record_ttl: The record TTL (number of seconds that the record may be cached). + :raises certbot.errors.PluginError: if an error occurs communicating with the DNS server + """ + + domain = self._find_domain(domain_name) + + n = dns.name.from_text(record_name) + o = dns.name.from_text(domain) + rel = n.relativize(o) + + update = dns.update.Update( + domain, + keyring=self.keyring, + keyalgorithm=self.algorithm) + update.add(rel, record_ttl, dns.rdatatype.TXT, record_content) + + try: + response = dns.query.tcp(update, self.server) + except Exception as e: + raise errors.PluginError('Encountered error adding TXT record: {0}' + .format(e)) + rcode = response.rcode() + + if rcode == dns.rcode.NOERROR: + logger.debug('Successfully added TXT record') + else: + raise errors.PluginError('Received response from server: {0}' + .format(dns.rcode.to_text(rcode))) + + def del_txt_record(self, domain_name, record_name, record_content): + """ + Delete a TXT record using the supplied information. + + :param str domain: The domain to use to find the closest SOA. + :param str record_name: The record name (typically beginning with '_acme-challenge.'). + :param str record_content: The record content (typically the challenge validation). + :param int record_ttl: The record TTL (number of seconds that the record may be cached). + :raises certbot.errors.PluginError: if an error occurs communicating with the DNS server + """ + + domain = self._find_domain(domain_name) + + n = dns.name.from_text(record_name) + o = dns.name.from_text(domain) + rel = n.relativize(o) + + update = dns.update.Update( + domain, + keyring=self.keyring, + keyalgorithm=self.algorithm) + update.delete(rel, dns.rdatatype.TXT, record_content) + + try: + response = dns.query.tcp(update, self.server) + except Exception as e: + raise errors.PluginError('Encountered error deleting TXT record: {0}' + .format(e)) + rcode = response.rcode() + + if rcode == dns.rcode.NOERROR: + logger.debug('Successfully deleted TXT record') + else: + raise errors.PluginError('Received response from server: {0}' + .format(dns.rcode.to_text(rcode))) + + def _find_domain(self, domain_name): + """ + Find the closest domain with an SOA record for a given domain name. + + :param str domain_name: The domain name for which to find the closest SOA record. + :returns: The domain, if found. + :rtype: str + :raises certbot.errors.PluginError: if no SOA record can be found. + """ + + domain_name_guesses = dns_common.base_domain_name_guesses(domain_name) + + # Loop through until we find an authoritative SOA record + for guess in domain_name_guesses: + if self._query_soa(guess): + return guess + + raise errors.PluginError('Unable to determine base domain for {0} using names: {1}.' + .format(domain_name, domain_name_guesses)) + + def _query_soa(self, domain_name): + """ + Query a domain name for an authoritative SOA record. + + :param str domain_name: The domain name to query for an SOA record. + :returns: True if found, False otherwise. + :rtype: bool + :raises certbot.errors.PluginError: if no response is received. + """ + + domain = dns.name.from_text(domain_name) + + request = dns.message.make_query(domain, dns.rdatatype.SOA, dns.rdataclass.IN) + # Turn off Recursion Desired bit in query + request.flags ^= dns.flags.RD + + try: + response = dns.query.udp(request, self.server) + rcode = response.rcode() + + # Authoritative Answer bit should be set + if (rcode == dns.rcode.NOERROR and len(response.answer) > 0 and + response.flags & dns.flags.AA): + logger.debug('Received authoritative SOA response for %s', domain_name) + return True + + logger.debug('No authoritative SOA record found for %s', domain_name) + return False + except Exception as e: + raise errors.PluginError('Encountered error when making query: {0}' + .format(e)) + diff --git a/certbot-dns-rfc2136/certbot_dns_rfc2136/dns_rfc2136_test.py b/certbot-dns-rfc2136/certbot_dns_rfc2136/dns_rfc2136_test.py new file mode 100644 index 000000000..54fdb8575 --- /dev/null +++ b/certbot-dns-rfc2136/certbot_dns_rfc2136/dns_rfc2136_test.py @@ -0,0 +1,197 @@ +"""Tests for certbot_dns_rfc2136.dns_rfc2136.""" + +import os +import unittest + +import dns.flags +import dns.rcode +import dns.tsig +import mock + +from certbot import errors +from certbot.plugins import dns_test_common +from certbot.plugins.dns_test_common import DOMAIN +from certbot.tests import util as test_util + +SERVER = '192.0.2.1' +NAME = 'a-tsig-key.' +SECRET = 'SSB3b25kZXIgd2hvIHdpbGwgYm90aGVyIHRvIGRlY29kZSB0aGlzIHRleHQK' +VALID_CONFIG = {"rfc2136_server": SERVER, "rfc2136_name": NAME, "rfc2136_secret": SECRET} + + +class AuthenticatorTest(test_util.TempDirTestCase, dns_test_common.BaseAuthenticatorTest): + + def setUp(self): + from certbot_dns_rfc2136.dns_rfc2136 import Authenticator + + super(AuthenticatorTest, self).setUp() + + path = os.path.join(self.tempdir, 'file.ini') + dns_test_common.write(VALID_CONFIG, path) + + self.config = mock.MagicMock(rfc2136_credentials=path, + rfc2136_propagation_seconds=0) # don't wait during tests + + self.auth = Authenticator(self.config, "rfc2136") + + self.mock_client = mock.MagicMock() + # _get_rfc2136_client | pylint: disable=protected-access + self.auth._get_rfc2136_client = mock.MagicMock(return_value=self.mock_client) + + def test_perform(self): + self.auth.perform([self.achall]) + + expected = [mock.call.add_txt_record(DOMAIN, '_acme-challenge.'+DOMAIN, mock.ANY, mock.ANY)] + self.assertEqual(expected, self.mock_client.mock_calls) + + def test_cleanup(self): + # _attempt_cleanup | pylint: disable=protected-access + self.auth._attempt_cleanup = True + self.auth.cleanup([self.achall]) + + expected = [mock.call.del_txt_record(DOMAIN, '_acme-challenge.'+DOMAIN, mock.ANY)] + self.assertEqual(expected, self.mock_client.mock_calls) + + def test_invalid_algorithm_raises(self): + config = VALID_CONFIG.copy() + config["rfc2136_algorithm"] = "INVALID" + dns_test_common.write(config, self.config.rfc2136_credentials) + + self.assertRaises(errors.PluginError, + self.auth.perform, + [self.achall]) + + def test_valid_algorithm_passes(self): + config = VALID_CONFIG.copy() + config["rfc2136_algorithm"] = "HMAC-SHA512" + dns_test_common.write(config, self.config.rfc2136_credentials) + + self.auth.perform([self.achall]) + + +class RFC2136ClientTest(unittest.TestCase): + + def setUp(self): + from certbot_dns_rfc2136.dns_rfc2136 import _RFC2136Client + + self.rfc2136_client = _RFC2136Client(SERVER, NAME, SECRET, dns.tsig.HMAC_MD5) + + @mock.patch("dns.query.tcp") + def test_add_txt_record(self, query_mock): + query_mock.return_value.rcode.return_value = dns.rcode.NOERROR + # _find_domain | pylint: disable=protected-access + self.rfc2136_client._find_domain = mock.MagicMock(return_value="example.com") + + self.rfc2136_client.add_txt_record(DOMAIN, "bar", "baz", 42) + + query_mock.assert_called_with(mock.ANY, SERVER) + self.assertTrue("bar. 42 IN TXT \"baz\"" in str(query_mock.call_args[0][0])) + + @mock.patch("dns.query.tcp") + def test_add_txt_record_wraps_errors(self, query_mock): + query_mock.side_effect = Exception + # _find_domain | pylint: disable=protected-access + self.rfc2136_client._find_domain = mock.MagicMock(return_value="example.com") + + self.assertRaises( + errors.PluginError, + self.rfc2136_client.add_txt_record, + DOMAIN, "bar", "baz", 42) + + @mock.patch("dns.query.tcp") + def test_add_txt_record_server_error(self, query_mock): + query_mock.return_value.rcode.return_value = dns.rcode.NXDOMAIN + # _find_domain | pylint: disable=protected-access + self.rfc2136_client._find_domain = mock.MagicMock(return_value="example.com") + + self.assertRaises( + errors.PluginError, + self.rfc2136_client.add_txt_record, + DOMAIN, "bar", "baz", 42) + + @mock.patch("dns.query.tcp") + def test_del_txt_record(self, query_mock): + query_mock.return_value.rcode.return_value = dns.rcode.NOERROR + # _find_domain | pylint: disable=protected-access + self.rfc2136_client._find_domain = mock.MagicMock(return_value="example.com") + + self.rfc2136_client.del_txt_record(DOMAIN, "bar", "baz") + + query_mock.assert_called_with(mock.ANY, SERVER) + self.assertTrue("bar. 0 NONE TXT \"baz\"" in str(query_mock.call_args[0][0])) + + @mock.patch("dns.query.tcp") + def test_del_txt_record_wraps_errors(self, query_mock): + query_mock.side_effect = Exception + # _find_domain | pylint: disable=protected-access + self.rfc2136_client._find_domain = mock.MagicMock(return_value="example.com") + + self.assertRaises( + errors.PluginError, + self.rfc2136_client.del_txt_record, + DOMAIN, "bar", "baz") + + @mock.patch("dns.query.tcp") + def test_del_txt_record_server_error(self, query_mock): + query_mock.return_value.rcode.return_value = dns.rcode.NXDOMAIN + # _find_domain | pylint: disable=protected-access + self.rfc2136_client._find_domain = mock.MagicMock(return_value="example.com") + + self.assertRaises( + errors.PluginError, + self.rfc2136_client.del_txt_record, + DOMAIN, "bar", "baz") + + def test_find_domain(self): + # _query_soa | pylint: disable=protected-access + self.rfc2136_client._query_soa = mock.MagicMock(side_effect=[False, False, True]) + + # _find_domain | pylint: disable=protected-access + domain = self.rfc2136_client._find_domain('foo.bar.'+DOMAIN) + + self.assertTrue(domain == DOMAIN) + + def test_find_domain_wraps_errors(self): + # _query_soa | pylint: disable=protected-access + self.rfc2136_client._query_soa = mock.MagicMock(return_value=False) + + self.assertRaises( + errors.PluginError, + # _find_domain | pylint: disable=protected-access + self.rfc2136_client._find_domain, + 'foo.bar.'+DOMAIN) + + @mock.patch("dns.query.udp") + def test_query_soa_found(self, query_mock): + query_mock.return_value = mock.MagicMock(answer=[mock.MagicMock()], flags=dns.flags.AA) + query_mock.return_value.rcode.return_value = dns.rcode.NOERROR + + # _query_soa | pylint: disable=protected-access + result = self.rfc2136_client._query_soa(DOMAIN) + + query_mock.assert_called_with(mock.ANY, SERVER) + self.assertTrue(result == True) + + @mock.patch("dns.query.udp") + def test_query_soa_not_found(self, query_mock): + query_mock.return_value.rcode.return_value = dns.rcode.NXDOMAIN + + # _query_soa | pylint: disable=protected-access + result = self.rfc2136_client._query_soa(DOMAIN) + + query_mock.assert_called_with(mock.ANY, SERVER) + self.assertTrue(result == False) + + @mock.patch("dns.query.udp") + def test_query_soa_wraps_errors(self, query_mock): + query_mock.side_effect = Exception + + self.assertRaises( + errors.PluginError, + # _query_soa | pylint: disable=protected-access + self.rfc2136_client._query_soa, + DOMAIN) + + +if __name__ == "__main__": + unittest.main() # pragma: no cover diff --git a/certbot-dns-rfc2136/docs/.gitignore b/certbot-dns-rfc2136/docs/.gitignore new file mode 100644 index 000000000..ba65b13af --- /dev/null +++ b/certbot-dns-rfc2136/docs/.gitignore @@ -0,0 +1 @@ +/_build/ diff --git a/certbot-dns-rfc2136/docs/Makefile b/certbot-dns-rfc2136/docs/Makefile new file mode 100644 index 000000000..c7aea1792 --- /dev/null +++ b/certbot-dns-rfc2136/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = python -msphinx +SPHINXPROJ = certbot-dns-rfc2136 +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) \ No newline at end of file diff --git a/certbot-dns-rfc2136/docs/api.rst b/certbot-dns-rfc2136/docs/api.rst new file mode 100644 index 000000000..8668ec5d8 --- /dev/null +++ b/certbot-dns-rfc2136/docs/api.rst @@ -0,0 +1,8 @@ +================= +API Documentation +================= + +.. toctree:: + :glob: + + api/** diff --git a/certbot-dns-rfc2136/docs/api/dns_rfc2136.rst b/certbot-dns-rfc2136/docs/api/dns_rfc2136.rst new file mode 100644 index 000000000..f5e98454a --- /dev/null +++ b/certbot-dns-rfc2136/docs/api/dns_rfc2136.rst @@ -0,0 +1,5 @@ +:mod:`certbot_dns_rfc2136.dns_rfc2136` +-------------------------------------- + +.. automodule:: certbot_dns_rfc2136.dns_rfc2136 + :members: diff --git a/certbot-dns-rfc2136/docs/conf.py b/certbot-dns-rfc2136/docs/conf.py new file mode 100644 index 000000000..8cc5d595f --- /dev/null +++ b/certbot-dns-rfc2136/docs/conf.py @@ -0,0 +1,180 @@ +# -*- coding: utf-8 -*- +# +# certbot-dns-rfc2136 documentation build configuration file, created by +# sphinx-quickstart on Thu Jun 15 06:42:51 2017. +# +# This file is execfile()d with the current directory set to its +# containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +import os +# import sys +# sys.path.insert(0, os.path.abspath('.')) + + +# -- General configuration ------------------------------------------------ + +# If your documentation needs a minimal Sphinx version, state it here. +# +needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = ['sphinx.ext.autodoc', + 'sphinx.ext.intersphinx', + 'sphinx.ext.todo', + 'sphinx.ext.coverage', + 'sphinx.ext.viewcode'] + +autodoc_member_order = 'bysource' +autodoc_default_flags = ['show-inheritance', 'private-members'] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# +# source_suffix = ['.rst', '.md'] +source_suffix = '.rst' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = u'certbot-dns-rfc2136' +copyright = u'2017, Certbot Project' +author = u'Certbot Project' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = u'0' +# The full version, including alpha/beta/rc tags. +release = u'0' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = 'en' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This patterns also effect to html_static_path and html_extra_path +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] + +default_role = 'py:obj' + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# If true, `todo` and `todoList` produce output, else they produce nothing. +todo_include_todos = True + + +# -- Options for HTML output ---------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# + +# http://docs.readthedocs.org/en/latest/theme.html#how-do-i-use-this-locally-and-on-read-the-docs +# on_rtd is whether we are on readthedocs.org +on_rtd = os.environ.get('READTHEDOCS', None) == 'True' +if not on_rtd: # only import and set the theme if we're building docs locally + import sphinx_rtd_theme + html_theme = 'sphinx_rtd_theme' + html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] +# otherwise, readthedocs.org uses their theme by default, so no need to specify it + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +# +# html_theme_options = {} + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + + +# -- Options for HTMLHelp output ------------------------------------------ + +# Output file base name for HTML help builder. +htmlhelp_basename = 'certbot-dns-rfc2136doc' + + +# -- Options for LaTeX output --------------------------------------------- + +latex_elements = { + # The paper size ('letterpaper' or 'a4paper'). + # + # 'papersize': 'letterpaper', + + # The font size ('10pt', '11pt' or '12pt'). + # + # 'pointsize': '10pt', + + # Additional stuff for the LaTeX preamble. + # + # 'preamble': '', + + # Latex figure (float) alignment + # + # 'figure_align': 'htbp', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + (master_doc, 'certbot-dns-rfc2136.tex', u'certbot-dns-rfc2136 Documentation', + u'Certbot Project', 'manual'), +] + + +# -- Options for manual page output --------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + (master_doc, 'certbot-dns-rfc2136', u'certbot-dns-rfc2136 Documentation', + [author], 1) +] + + +# -- Options for Texinfo output ------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + (master_doc, 'certbot-dns-rfc2136', u'certbot-dns-rfc2136 Documentation', + author, 'certbot-dns-rfc2136', 'One line description of project.', + 'Miscellaneous'), +] + + + + +# Example configuration for intersphinx: refer to the Python standard library. +intersphinx_mapping = { + 'python': ('https://docs.python.org/', None), + 'acme': ('https://acme-python.readthedocs.org/en/latest/', None), + 'certbot': ('https://certbot.eff.org/docs/', None), +} diff --git a/certbot-dns-rfc2136/docs/index.rst b/certbot-dns-rfc2136/docs/index.rst new file mode 100644 index 000000000..71705cb7f --- /dev/null +++ b/certbot-dns-rfc2136/docs/index.rst @@ -0,0 +1,28 @@ +.. certbot-dns-rfc2136 documentation master file, created by + sphinx-quickstart on Thu Jun 15 06:42:51 2017. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to certbot-dns-rfc2136's documentation! +=============================================== + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + +.. toctree:: + :maxdepth: 1 + + api + +.. automodule:: certbot_dns_rfc2136 + :members: + + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/certbot-dns-rfc2136/docs/make.bat b/certbot-dns-rfc2136/docs/make.bat new file mode 100644 index 000000000..8d09ca8fd --- /dev/null +++ b/certbot-dns-rfc2136/docs/make.bat @@ -0,0 +1,36 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=python -msphinx +) +set SOURCEDIR=. +set BUILDDIR=_build +set SPHINXPROJ=certbot-dns-rfc2136 + +if "%1" == "" goto help + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The Sphinx module was not found. Make sure you have Sphinx installed, + echo.then set the SPHINXBUILD environment variable to point to the full + echo.path of the 'sphinx-build' executable. Alternatively you may add the + echo.Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% + +:end +popd diff --git a/certbot-dns-rfc2136/setup.cfg b/certbot-dns-rfc2136/setup.cfg new file mode 100644 index 000000000..2a9acf13d --- /dev/null +++ b/certbot-dns-rfc2136/setup.cfg @@ -0,0 +1,2 @@ +[bdist_wheel] +universal = 1 diff --git a/certbot-dns-rfc2136/setup.py b/certbot-dns-rfc2136/setup.py new file mode 100644 index 000000000..3b5409602 --- /dev/null +++ b/certbot-dns-rfc2136/setup.py @@ -0,0 +1,69 @@ +import sys + +from setuptools import setup +from setuptools import find_packages + + +version = '0.17.0.dev0' + +# Please update tox.ini when modifying dependency version requirements +install_requires = [ + 'acme=={0}'.format(version), + 'certbot=={0}'.format(version), + 'dnspython', + 'mock', + # For pkg_resources. >=1.0 so pip resolves it to a version cryptography + # will tolerate; see #2599: + 'setuptools>=1.0', + 'zope.interface', +] + +docs_extras = [ + 'Sphinx>=1.0', # autodoc_member_order = 'bysource', autodoc_default_flags + 'sphinx_rtd_theme', +] + +setup( + name='certbot-dns-rfc2136', + version=version, + description="RFC 2136 DNS Authenticator plugin for Certbot", + url='https://github.com/certbot/certbot', + author="Certbot Project", + author_email='client-dev@letsencrypt.org', + license='Apache License 2.0', + classifiers=[ + 'Development Status :: 3 - Alpha', + 'Environment :: Plugins', + 'Intended Audience :: System Administrators', + 'License :: OSI Approved :: Apache Software License', + 'Operating System :: POSIX :: Linux', + 'Programming Language :: Python', + 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 2.6', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.3', + 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', + 'Topic :: Internet :: WWW/HTTP', + 'Topic :: Security', + 'Topic :: System :: Installation/Setup', + 'Topic :: System :: Networking', + 'Topic :: System :: Systems Administration', + 'Topic :: Utilities', + ], + + packages=find_packages(), + include_package_data=True, + install_requires=install_requires, + extras_require={ + 'docs': docs_extras, + }, + entry_points={ + 'certbot.plugins': [ + 'dns-rfc2136 = certbot_dns_rfc2136.dns_rfc2136:Authenticator', + ], + }, + test_suite='certbot_dns_rfc2136', +) diff --git a/certbot-route53/.gitignore b/certbot-dns-route53/.gitignore similarity index 100% rename from certbot-route53/.gitignore rename to certbot-dns-route53/.gitignore diff --git a/certbot-route53/LICENSE b/certbot-dns-route53/LICENSE similarity index 100% rename from certbot-route53/LICENSE rename to certbot-dns-route53/LICENSE diff --git a/certbot-route53/MANIFEST.in b/certbot-dns-route53/MANIFEST.in similarity index 55% rename from certbot-route53/MANIFEST.in rename to certbot-dns-route53/MANIFEST.in index 9575a1c62..c48c07e59 100644 --- a/certbot-route53/MANIFEST.in +++ b/certbot-dns-route53/MANIFEST.in @@ -1,2 +1,3 @@ include LICENSE include README +recursive-include docs * diff --git a/certbot-route53/README.md b/certbot-dns-route53/README.md similarity index 97% rename from certbot-route53/README.md rename to certbot-dns-route53/README.md index 582a0fb35..4af66aa00 100644 --- a/certbot-route53/README.md +++ b/certbot-dns-route53/README.md @@ -30,6 +30,6 @@ To generate a certificate: ``` certbot certonly \ -n --agree-tos --email DEVOPS@COMPANY.COM \ - -a certbot-route53:auth \ + --dns-route53 \ -d MY.DOMAIN.NAME ``` diff --git a/certbot-dns-route53/certbot_dns_route53/__init__.py b/certbot-dns-route53/certbot_dns_route53/__init__.py new file mode 100644 index 000000000..8659617ef --- /dev/null +++ b/certbot-dns-route53/certbot_dns_route53/__init__.py @@ -0,0 +1,120 @@ +""" +The `~certbot_dns_route53.dns_route53` plugin automates the process of +completing a ``dns-01`` challenge (`~acme.challenges.DNS01`) by creating, and +subsequently removing, TXT records using the Amazon Web Services Route 53 API. + + +Named Arguments +--------------- + +======================================== ===================================== +``--dns-route53-propagation-seconds`` The number of seconds to wait for DNS + to propagate before asking the ACME + server to verify the DNS record. + (Default: 10) +======================================== ===================================== + + +Credentials +----------- +Use of this plugin requires a configuration file containing Amazon Web Sevices +API credentials for an account with the following permissions: + +* ``route53:ListHostedZones`` +* ``route53:GetChange`` +* ``route53:ChangeResourceRecordSets`` + +These permissions can be captured in an AWS policy like the one below. Amazon +provides `information about managing access `_ and `information about +the required permissions `_ + +.. code-block:: json + :name: sample-aws-policy.json + :caption: Example AWS policy file: + + { + "Version": "2012-10-17", + "Id": "certbot-dns-route53 sample policy", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "route53:ListHostedZones", + "route53:GetChange" + ], + "Resource": [ + "*" + ] + }, + { + "Effect" : "Allow", + "Action" : [ + "route53:ChangeResourceRecordSets" + ], + "Resource" : [ + "arn:aws:route53:::hostedzone/YOURHOSTEDZONEID" + ] + } + ] + } + +The `access keys `_ for an account +with these permissions must be supplied in one of the following ways, which are +discussed in more detail in the Boto3 library's documentation about `configuring +credentials `_. + +* Using the ``AWS_ACCESS_KEY_ID`` and ``AWS_SECRET_ACCESS_KEY`` environment + variables. +* Using a credentials configuration file at the default location, + ``~/.aws/config``. +* Using a credentials configuration file at a path supplied using the + ``AWS_CONFIG_FILE`` environment variable. + +.. code-block:: ini + :name: config.ini + :caption: Example credentials config file: + + [default] + aws_access_key_id=AKIAIOSFODNN7EXAMPLE + aws_secret_access_key=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY + +.. caution:: + You should protect these API credentials as you would a password. Users who + can read this file can use these credentials to issue some types of API calls + on your behalf, limited by the permissions assigned to the account. Users who + can cause Certbot to run using these credentials can complete a ``dns-01`` + challenge to acquire new certificates or revoke existing certificates for + domains these credentials are authorized to manage. + + +Examples +-------- +.. code-block:: bash + :caption: To acquire a certificate for ``example.com`` + + certbot certonly \\ + --dns-route53 \\ + -d example.com + +.. code-block:: bash + :caption: To acquire a single certificate for both ``example.com`` and + ``www.example.com`` + + certbot certonly \\ + --dns-route53 \\ + -d example.com \\ + -d www.example.com + +.. code-block:: bash + :caption: To acquire a certificate for ``example.com``, waiting 30 seconds + for DNS propagation + + certbot certonly \\ + --dns-route53 \\ + --dns-route53-propagation-seconds 30 \\ + -d example.com +""" diff --git a/certbot-dns-route53/certbot_dns_route53/authenticator.py b/certbot-dns-route53/certbot_dns_route53/authenticator.py new file mode 100644 index 000000000..53215ea1d --- /dev/null +++ b/certbot-dns-route53/certbot_dns_route53/authenticator.py @@ -0,0 +1,20 @@ +"""Shim around `~certbot_dns_route53.dns_route53` for backwards compatibility.""" +import warnings + +import zope.interface + +from certbot import interfaces +from certbot_dns_route53 import dns_route53 + + +@zope.interface.implementer(interfaces.IAuthenticator) +@zope.interface.provider(interfaces.IPluginFactory) +class Authenticator(dns_route53.Authenticator): + """Shim around `~certbot_dns_route53.dns_route53.Authenticator` for backwards compatibility.""" + + hidden = True + + def __init__(self, *args, **kwargs): + warnings.warn("The 'authenticator' module was renamed 'dns_route53'", + DeprecationWarning) + super(Authenticator, self).__init__(*args, **kwargs) diff --git a/certbot-route53/certbot_route53/authenticator.py b/certbot-dns-route53/certbot_dns_route53/dns_route53.py similarity index 61% rename from certbot-route53/certbot_route53/authenticator.py rename to certbot-dns-route53/certbot_dns_route53/dns_route53.py index d53fd102f..67462e369 100644 --- a/certbot-route53/certbot_route53/authenticator.py +++ b/certbot-dns-route53/certbot_dns_route53/dns_route53.py @@ -4,71 +4,56 @@ import time import boto3 import zope.interface -from acme import challenges from botocore.exceptions import NoCredentialsError, ClientError +from certbot import errors from certbot import interfaces -from certbot.plugins import common +from certbot.plugins import dns_common logger = logging.getLogger(__name__) -TTL = 10 - INSTRUCTIONS = ( - "To use certbot-route53, configure credentials as described at " + "To use certbot-dns-route53, configure credentials as described at " "https://boto3.readthedocs.io/en/latest/guide/configuration.html#best-practices-for-configuring-credentials " # pylint: disable=line-too-long "and add the necessary permissions for Route53 access.") @zope.interface.implementer(interfaces.IAuthenticator) @zope.interface.provider(interfaces.IPluginFactory) -class Authenticator(common.Plugin): +class Authenticator(dns_common.DNSAuthenticator): """Route53 Authenticator This authenticator solves a DNS01 challenge by uploading the answer to AWS Route53. """ - description = ("Authenticate domain names using the DNS challenge type, " - "by automatically updating TXT records using AWS Route53. Works only " - "if you use AWS Route53 to host DNS for your domains. " + - INSTRUCTIONS) + description = ("Obtain certificates using a DNS TXT record (if you are using AWS Route53 for " + "DNS).") + ttl = 10 def __init__(self, *args, **kwargs): super(Authenticator, self).__init__(*args, **kwargs) self.r53 = boto3.client("route53") - def prepare(self): # pylint: disable=missing-docstring,no-self-use - pass # pragma: no cover - def more_info(self): # pylint: disable=missing-docstring,no-self-use return "Solve a DNS01 challenge using AWS Route53" - def get_chall_pref(self, domain): - # pylint: disable=missing-docstring,no-self-use,unused-argument - return [challenges.DNS01] + def _setup_credentials(self): + pass - def perform(self, achalls): # pylint: disable=missing-docstring + def _perform(self, domain, validation_domain_name, validation): try: - change_ids = [ - self._change_txt_record("UPSERT", achall) - for achall in achalls - ] + change_id = self._change_txt_record("UPSERT", validation_domain_name, validation) - for change_id in change_ids: - self._wait_for_change(change_id) - # Sleep for at least the TTL, to ensure that any records cached by - # the ACME server after previous validation attempts are gone. In - # most cases we'll need to wait at least this long for the Route53 - # records to propagate, so this doesn't delay us much. - time.sleep(TTL) - return [achall.response(achall.account_key) for achall in achalls] + self._wait_for_change(change_id) except (NoCredentialsError, ClientError) as e: - e.args = ("\n".join([str(e), INSTRUCTIONS]),) - raise + logger.debug('Encountered error during perform: %s', e, exc_info=True) + raise errors.PluginError("\n".join([str(e), INSTRUCTIONS])) - def cleanup(self, achalls): # pylint: disable=missing-docstring - for achall in achalls: - self._change_txt_record("DELETE", achall) + def _cleanup(self, domain, validation_domain_name, validation): + try: + self._change_txt_record("DELETE", validation_domain_name, validation) + except (NoCredentialsError, ClientError) as e: + logger.debug('Encountered error during cleanup: %s', e, exc_info=True) def _find_zone_id_for_domain(self, domain): """Find the zone id responsible a given FQDN. @@ -89,7 +74,7 @@ class Authenticator(common.Plugin): zones.append((zone["Name"], zone["Id"])) if not zones: - raise ValueError( + raise errors.PluginError( "Unable to find a Route53 hosted zone for {0}".format(domain) ) @@ -100,27 +85,24 @@ class Authenticator(common.Plugin): zones.sort(key=lambda z: len(z[0]), reverse=True) return zones[0][1] - def _change_txt_record(self, action, achall): - domain = achall.validation_domain_name(achall.domain) - value = achall.validation(achall.account_key) - - zone_id = self._find_zone_id_for_domain(domain) + def _change_txt_record(self, action, validation_domain_name, validation): + zone_id = self._find_zone_id_for_domain(validation_domain_name) response = self.r53.change_resource_record_sets( HostedZoneId=zone_id, ChangeBatch={ - "Comment": "certbot-route53 certificate validation " + action, + "Comment": "certbot-dns-route53 certificate validation " + action, "Changes": [ { "Action": action, "ResourceRecordSet": { - "Name": domain, + "Name": validation_domain_name, "Type": "TXT", - "TTL": TTL, + "TTL": self.ttl, "ResourceRecords": [ # For some reason TXT records need to be # manually quoted. - {"Value": '"{0}"'.format(value)} + {"Value": '"{0}"'.format(validation)} ], } } @@ -138,6 +120,6 @@ class Authenticator(common.Plugin): if response["ChangeInfo"]["Status"] == "INSYNC": return time.sleep(5) - raise Exception( + raise errors.PluginError( "Timed out waiting for Route53 change. Current status: %s" % response["ChangeInfo"]["Status"]) diff --git a/certbot-route53/certbot_route53/authenticator_test.py b/certbot-dns-route53/certbot_dns_route53/dns_route53_test.py similarity index 83% rename from certbot-route53/certbot_route53/authenticator_test.py rename to certbot-dns-route53/certbot_dns_route53/dns_route53_test.py index 545fd01a4..ff07b6ccd 100644 --- a/certbot-route53/certbot_route53/authenticator_test.py +++ b/certbot-dns-route53/certbot_dns_route53/dns_route53_test.py @@ -1,19 +1,20 @@ -"""Tests for certbot_route53.authenticator""" +"""Tests for certbot_dns_route53.dns_route53.Authenticator""" import unittest import mock - from botocore.exceptions import NoCredentialsError, ClientError +from certbot import errors from certbot.plugins import dns_test_common +from certbot.plugins.dns_test_common import DOMAIN class AuthenticatorTest(unittest.TestCase, dns_test_common.BaseAuthenticatorTest): # pylint: disable=protected-access def setUp(self): - from certbot_route53.authenticator import Authenticator + from certbot_dns_route53.dns_route53 import Authenticator super(AuthenticatorTest, self).setUp() @@ -21,22 +22,21 @@ class AuthenticatorTest(unittest.TestCase, dns_test_common.BaseAuthenticatorTest self.auth = Authenticator(self.config, "route53") - def test_parser_arguments(self): - pass # TODO: follow convention of defining optional argument for DNS propagation delay - def test_perform(self): self.auth._change_txt_record = mock.MagicMock() self.auth._wait_for_change = mock.MagicMock() self.auth.perform([self.achall]) - self.auth._change_txt_record.assert_called_once_with("UPSERT", self.achall) + self.auth._change_txt_record.assert_called_once_with("UPSERT", + '_acme-challenge.' + DOMAIN, + mock.ANY) self.auth._wait_for_change.assert_called_once() def test_perform_no_credentials_error(self): self.auth._change_txt_record = mock.MagicMock(side_effect=NoCredentialsError) - self.assertRaises(NoCredentialsError, # TODO: Should be `errors.PluginError` + self.assertRaises(errors.PluginError, self.auth.perform, [self.achall]) @@ -44,31 +44,35 @@ class AuthenticatorTest(unittest.TestCase, dns_test_common.BaseAuthenticatorTest self.auth._change_txt_record = mock.MagicMock( side_effect=ClientError({"Error": {"Code": "foo"}}, "bar")) - self.assertRaises(ClientError, # TODO: Should be `errors.PluginError` + self.assertRaises(errors.PluginError, self.auth.perform, [self.achall]) def test_cleanup(self): + self.auth._attempt_cleanup = True + self.auth._change_txt_record = mock.MagicMock() self.auth.cleanup([self.achall]) - self.auth._change_txt_record.assert_called_once_with("DELETE", self.achall) + self.auth._change_txt_record.assert_called_once_with("DELETE", + '_acme-challenge.'+DOMAIN, + mock.ANY) def test_cleanup_no_credentials_error(self): + self.auth._attempt_cleanup = True + self.auth._change_txt_record = mock.MagicMock(side_effect=NoCredentialsError) - self.assertRaises(NoCredentialsError, # TODO: Should not raise - self.auth.cleanup, - [self.achall]) + self.auth.cleanup([self.achall]) def test_cleanup_client_error(self): + self.auth._attempt_cleanup = True + self.auth._change_txt_record = mock.MagicMock( side_effect=ClientError({"Error": {"Code": "foo"}}, "bar")) - self.assertRaises(ClientError, # TODO: Should not raise - self.auth.cleanup, - [self.achall]) + self.auth.cleanup([self.achall]) class ClientTest(unittest.TestCase): @@ -107,7 +111,7 @@ class ClientTest(unittest.TestCase): } def setUp(self): - from certbot_route53.authenticator import Authenticator + from certbot_dns_route53.dns_route53 import Authenticator super(ClientTest, self).setUp() @@ -153,7 +157,7 @@ class ClientTest(unittest.TestCase): self.client.r53.get_paginator = mock.MagicMock() self.client.r53.get_paginator().paginate.return_value = [] - self.assertRaises(ValueError, # TODO: Should be `errors.PluginError` + self.assertRaises(errors.PluginError, self.client._find_zone_id_for_domain, "foo.example.com") @@ -168,7 +172,7 @@ class ClientTest(unittest.TestCase): }, ] - self.assertRaises(ValueError, # TODO: Should be `errors.PluginError` + self.assertRaises(errors.PluginError, self.client._find_zone_id_for_domain, "foo.example.com") @@ -177,7 +181,7 @@ class ClientTest(unittest.TestCase): self.client.r53.change_resource_record_sets = mock.MagicMock( return_value={"ChangeInfo": {"Id": 1}}) - self.client._change_txt_record("FOO", dns_test_common.BaseAuthenticatorTest.achall) + self.client._change_txt_record("FOO", DOMAIN, "foo") self.client.r53.change_resource_record_sets.assert_called_once() diff --git a/certbot-dns-route53/docs/.gitignore b/certbot-dns-route53/docs/.gitignore new file mode 100644 index 000000000..ba65b13af --- /dev/null +++ b/certbot-dns-route53/docs/.gitignore @@ -0,0 +1 @@ +/_build/ diff --git a/certbot-dns-route53/docs/Makefile b/certbot-dns-route53/docs/Makefile new file mode 100644 index 000000000..25be9609c --- /dev/null +++ b/certbot-dns-route53/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +SPHINXPROJ = certbot-dns-route53 +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) \ No newline at end of file diff --git a/certbot-dns-route53/docs/api.rst b/certbot-dns-route53/docs/api.rst new file mode 100644 index 000000000..8668ec5d8 --- /dev/null +++ b/certbot-dns-route53/docs/api.rst @@ -0,0 +1,8 @@ +================= +API Documentation +================= + +.. toctree:: + :glob: + + api/** diff --git a/certbot-dns-route53/docs/api/authenticator.rst b/certbot-dns-route53/docs/api/authenticator.rst new file mode 100644 index 000000000..2d96a419b --- /dev/null +++ b/certbot-dns-route53/docs/api/authenticator.rst @@ -0,0 +1,5 @@ +:mod:`certbot_dns_route53.authenticator` +---------------------------------------- + +.. automodule:: certbot_dns_route53.authenticator + :members: diff --git a/certbot-dns-route53/docs/api/dns_route53.rst b/certbot-dns-route53/docs/api/dns_route53.rst new file mode 100644 index 000000000..7573f2e19 --- /dev/null +++ b/certbot-dns-route53/docs/api/dns_route53.rst @@ -0,0 +1,5 @@ +:mod:`certbot_dns_route53.dns_route53` +-------------------------------------- + +.. automodule:: certbot_dns_route53.dns_route53 + :members: diff --git a/certbot-dns-route53/docs/conf.py b/certbot-dns-route53/docs/conf.py new file mode 100644 index 000000000..25a7c6e4d --- /dev/null +++ b/certbot-dns-route53/docs/conf.py @@ -0,0 +1,180 @@ +# -*- coding: utf-8 -*- +# +# certbot-dns-route53 documentation build configuration file, created by +# sphinx-quickstart on Fri Jun 9 11:45:30 2017. +# +# This file is execfile()d with the current directory set to its +# containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +import os +# import sys +# sys.path.insert(0, os.path.abspath('.')) + + +# -- General configuration ------------------------------------------------ + +# If your documentation needs a minimal Sphinx version, state it here. +# +needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = ['sphinx.ext.autodoc', + 'sphinx.ext.intersphinx', + 'sphinx.ext.todo', + 'sphinx.ext.coverage', + 'sphinx.ext.viewcode'] + +autodoc_member_order = 'bysource' +autodoc_default_flags = ['show-inheritance', 'private-members'] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# +# source_suffix = ['.rst', '.md'] +source_suffix = '.rst' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = u'certbot-dns-route53' +copyright = u'2017, Certbot Project' +author = u'Certbot Project' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = u'0' +# The full version, including alpha/beta/rc tags. +release = u'0' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = 'en' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This patterns also effect to html_static_path and html_extra_path +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] + +default_role = 'py:obj' + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# If true, `todo` and `todoList` produce output, else they produce nothing. +todo_include_todos = True + + +# -- Options for HTML output ---------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# + +# http://docs.readthedocs.org/en/latest/theme.html#how-do-i-use-this-locally-and-on-read-the-docs +# on_rtd is whether we are on readthedocs.org +on_rtd = os.environ.get('READTHEDOCS', None) == 'True' +if not on_rtd: # only import and set the theme if we're building docs locally + import sphinx_rtd_theme + html_theme = 'sphinx_rtd_theme' + html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] +# otherwise, readthedocs.org uses their theme by default, so no need to specify it + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +# +# html_theme_options = {} + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + + +# -- Options for HTMLHelp output ------------------------------------------ + +# Output file base name for HTML help builder. +htmlhelp_basename = 'certbot-dns-route53doc' + + +# -- Options for LaTeX output --------------------------------------------- + +latex_elements = { + # The paper size ('letterpaper' or 'a4paper'). + # + # 'papersize': 'letterpaper', + + # The font size ('10pt', '11pt' or '12pt'). + # + # 'pointsize': '10pt', + + # Additional stuff for the LaTeX preamble. + # + # 'preamble': '', + + # Latex figure (float) alignment + # + # 'figure_align': 'htbp', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + (master_doc, 'certbot-dns-route53.tex', u'certbot-dns-route53 Documentation', + u'Certbot Project', 'manual'), +] + + +# -- Options for manual page output --------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + (master_doc, 'certbot-dns-route53', u'certbot-dns-route53 Documentation', + [author], 1) +] + + +# -- Options for Texinfo output ------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + (master_doc, 'certbot-dns-route53', u'certbot-dns-route53 Documentation', + author, 'certbot-dns-route53', 'One line description of project.', + 'Miscellaneous'), +] + + + + +# Example configuration for intersphinx: refer to the Python standard library. +intersphinx_mapping = { + 'python': ('https://docs.python.org/', None), + 'acme': ('https://acme-python.readthedocs.org/en/latest/', None), + 'certbot': ('https://certbot.eff.org/docs/', None), +} diff --git a/certbot-dns-route53/docs/index.rst b/certbot-dns-route53/docs/index.rst new file mode 100644 index 000000000..bacf73150 --- /dev/null +++ b/certbot-dns-route53/docs/index.rst @@ -0,0 +1,28 @@ +.. certbot-dns-route53 documentation master file, created by + sphinx-quickstart on Fri Jun 9 11:45:30 2017. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to certbot-dns-route53's documentation! +=============================================== + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + +.. toctree:: + :maxdepth: 1 + + api + +.. automodule:: certbot_dns_route53 + :members: + + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/certbot-dns-route53/docs/make.bat b/certbot-dns-route53/docs/make.bat new file mode 100644 index 000000000..e92b5909a --- /dev/null +++ b/certbot-dns-route53/docs/make.bat @@ -0,0 +1,36 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=. +set BUILDDIR=_build +set SPHINXPROJ=certbot-dns-route53 + +if "%1" == "" goto help + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% + +:end +popd diff --git a/certbot-route53/examples/sample-aws-policy.json b/certbot-dns-route53/examples/sample-aws-policy.json similarity index 91% rename from certbot-route53/examples/sample-aws-policy.json rename to certbot-dns-route53/examples/sample-aws-policy.json index 0b4dcae41..10a17de19 100644 --- a/certbot-route53/examples/sample-aws-policy.json +++ b/certbot-dns-route53/examples/sample-aws-policy.json @@ -1,6 +1,6 @@ { "Version": "2012-10-17", - "Id": "certbot-route53 sample policy", + "Id": "certbot-dns-route53 sample policy", "Statement": [ { "Effect": "Allow", diff --git a/certbot-route53/setup.cfg b/certbot-dns-route53/setup.cfg similarity index 100% rename from certbot-route53/setup.cfg rename to certbot-dns-route53/setup.cfg diff --git a/certbot-route53/setup.py b/certbot-dns-route53/setup.py similarity index 87% rename from certbot-route53/setup.py rename to certbot-dns-route53/setup.py index 40a104a40..7a69f9a13 100644 --- a/certbot-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.15.0.dev0' +version = '0.17.0.dev0' install_requires = [ 'acme=={0}'.format(version), @@ -17,7 +17,7 @@ install_requires = [ ] setup( - name='certbot-route53', + name='certbot-dns-route53', version=version, description="Route53 DNS Authenticator plugin for Certbot", url='https://github.com/certbot/certbot', @@ -52,8 +52,9 @@ setup( keywords=['certbot', 'route53', 'aws'], entry_points={ 'certbot.plugins': [ - 'auth = certbot_route53.authenticator:Authenticator' + 'dns-route53 = certbot_dns_route53.dns_route53:Authenticator', + 'certbot-route53:auth = certbot_dns_route53.authenticator:Authenticator' ], }, - test_suite='certbot_route53', + test_suite='certbot_dns_route53', ) diff --git a/certbot-route53/tools/tester.pkoch-macos_sierra.sh b/certbot-dns-route53/tools/tester.pkoch-macos_sierra.sh similarity index 100% rename from certbot-route53/tools/tester.pkoch-macos_sierra.sh rename to certbot-dns-route53/tools/tester.pkoch-macos_sierra.sh diff --git a/certbot-nginx/certbot_nginx/constants.py b/certbot-nginx/certbot_nginx/constants.py index a74f97662..2e72b8686 100644 --- a/certbot-nginx/certbot_nginx/constants.py +++ b/certbot-nginx/certbot_nginx/constants.py @@ -27,6 +27,7 @@ ALL_SSL_OPTIONS_HASHES = [ 'a6d9f1c7d6b36749b52ba061fff1421f9a0a3d2cfdafbd63c05d06f65b990937', '7f95624dd95cf5afc708b9f967ee83a24b8025dc7c8d9df2b556bbc64256b3ff', '394732f2bbe3e5e637c3fb5c6e980a1f1b90b01e2e8d6b7cff41dde16e2a756d', + '4b16fec2bcbcd8a2f3296d886f17f9953ffdcc0af54582452ca1e52f5f776f16', ] """SHA256 hashes of the contents of all versions of MOD_SSL_CONF_SRC""" diff --git a/certbot-nginx/certbot_nginx/options-ssl-nginx.conf b/certbot-nginx/certbot_nginx/options-ssl-nginx.conf index 7303f9bc6..292d42984 100644 --- a/certbot-nginx/certbot_nginx/options-ssl-nginx.conf +++ b/certbot-nginx/certbot_nginx/options-ssl-nginx.conf @@ -1,7 +1,8 @@ -# This file contains important security parameters. If you modify this file manually, -# Certbot will be unable to automatically provide future security updates. -# Instead, you will need to manually update this file by referencing the contents of -# options-ssl-nginx.conf.new. +# This file contains important security parameters. If you modify this file +# manually, Certbot will be unable to automatically provide future security +# updates. Instead, Certbot will print and log an error message with a path to +# the up-to-date file that you will need to refer to when manually updating +# this file. ssl_session_cache shared:le_nginx_SSL:1m; ssl_session_timeout 1440m; diff --git a/certbot-nginx/certbot_nginx/tests/util.py b/certbot-nginx/certbot_nginx/tests/util.py index 2ee38ec38..6e1b0d8ff 100644 --- a/certbot-nginx/certbot_nginx/tests/util.py +++ b/certbot-nginx/certbot_nginx/tests/util.py @@ -65,7 +65,6 @@ def get_nginx_configurator( in_progress_dir=os.path.join(backups, "IN_PROGRESS"), server="https://acme-server.org:443/new", tls_sni_01_port=5001, - dry_run=False, ), name="nginx", version=version) diff --git a/certbot-nginx/setup.py b/certbot-nginx/setup.py index 4329eb858..b501aecd9 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.15.0.dev0' +version = '0.17.0.dev0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot-nginx/tests/boulder-integration.sh b/certbot-nginx/tests/boulder-integration.sh index bd35aee21..996cc2201 100755 --- a/certbot-nginx/tests/boulder-integration.sh +++ b/certbot-nginx/tests/boulder-integration.sh @@ -6,14 +6,18 @@ export PATH="/usr/sbin:$PATH" # /usr/sbin/nginx nginx_root="$root/nginx" mkdir $nginx_root -root="$nginx_root" ./certbot-nginx/tests/boulder-integration.conf.sh > $nginx_root/nginx.conf +original=$(root="$nginx_root" ./certbot-nginx/tests/boulder-integration.conf.sh) +nginx_conf="$nginx_root/nginx.conf" +echo "$original" > $nginx_conf + killall nginx || true nginx -c $nginx_root/nginx.conf certbot_test_nginx () { certbot_test \ - --configurator nginx \ + --authenticator nginx \ + --installer nginx \ --nginx-server-root $nginx_root \ "$@" } @@ -23,6 +27,9 @@ echo | openssl s_client -connect localhost:5001 \ | openssl x509 -out $root/nginx.pem diff -q $root/nginx.pem $root/conf/live/nginx.wtf/cert.pem +certbot_test_nginx rollback --checkpoints 9001 +diff -q <(echo "$original") $nginx_conf + # note: not reached if anything above fails, hence "killall" at the # top nginx -c $nginx_root/nginx.conf -s stop diff --git a/certbot-route53/certbot_route53/__init__.py b/certbot-route53/certbot_route53/__init__.py deleted file mode 100644 index c91c79c22..000000000 --- a/certbot-route53/certbot_route53/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Certbot Route53 plugin.""" diff --git a/certbot/__init__.py b/certbot/__init__.py index 5128bf096..23ed7eb7a 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.15.0.dev0' +__version__ = '0.17.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/auth_handler.py b/certbot/auth_handler.py index a1f23a895..5f520cbcb 100644 --- a/certbot/auth_handler.py +++ b/certbot/auth_handler.py @@ -444,7 +444,7 @@ def _report_no_chall_path(): _ERROR_HELP_COMMON = ( "To fix these errors, please make sure that your domain name was entered " - "correctly and the DNS A record(s) for that domain contain(s) the " + "correctly and the DNS A/AAAA record(s) for that domain contain(s) the " "right IP address.") diff --git a/certbot/cert_manager.py b/certbot/cert_manager.py index 6519e7068..207e4d072 100644 --- a/certbot/cert_manager.py +++ b/certbot/cert_manager.py @@ -157,7 +157,7 @@ def _get_certname(config, verb): if not choices: raise errors.Error("No existing certificates found.") code, index = disp.menu("Which certificate would you like to {0}?".format(verb), - choices, ok_label="Select", flag="--cert-name", + choices, flag="--cert-name", force_interactive=True) if code != display_util.OK or not index in range(0, len(choices)): raise errors.Error("User ended interaction.") @@ -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 1023d9aca..eaafce762 100644 --- a/certbot/cli.py +++ b/certbot/cli.py @@ -357,7 +357,8 @@ VERB_HELP = [ }), ("delete", { "short": "Clean up all files related to a certificate", - "opts": "Options for deleting a certificate" + "opts": "Options for deleting a certificate", + "usage": "\n\n certbot delete --cert-name CERTNAME\n\n" }), ("revoke", { "short": "Revoke a certificate specified with --cert-path", @@ -366,33 +367,41 @@ VERB_HELP = [ }), ("register", { "short": "Register for account with Let's Encrypt / other ACME server", - "opts": "Options for account registration & modification" + "opts": "Options for account registration & modification", + "usage": "\n\n certbot register --email user@example.com [options]\n\n" }), ("unregister", { "short": "Irrevocably deactivate your account", - "opts": "Options for account deactivation." + "opts": "Options for account deactivation.", + "usage": "\n\n certbot unregister [options]\n\n" }), ("install", { "short": "Install an arbitrary certificate in a server", - "opts": "Options for modifying how a certificate is deployed" + "opts": "Options for modifying how a certificate is deployed", + "usage": "\n\n certbot install --cert-path /path/to/fullchain.pem " + " --key-path /path/to/private-key [options]\n\n" }), ("config_changes", { "short": "Show changes that Certbot has made to server configurations", - "opts": "Options for controlling which changes are displayed" + "opts": "Options for controlling which changes are displayed", + "usage": "\n\n certbot config_changes --num NUM [options]\n\n" }), ("rollback", { "short": "Roll back server conf changes made during certificate installation", - "opts": "Options for rolling back server configuration changes" + "opts": "Options for rolling back server configuration changes", + "usage": "\n\n certbot rollback --checkpoints 3 [options]\n\n" }), ("plugins", { "short": "List plugins that are installed and available on your system", - "opts": 'Options for for the "plugins" subcommand' + "opts": 'Options for for the "plugins" subcommand', + "usage": "\n\n certbot plugins [options]\n\n" }), ("update_symlinks", { "short": "Recreate symlinks in your /etc/letsencrypt/live/ directory", "opts": ("Recreates certificate and key symlinks in {0}, if you changed them by hand " "or edited a renewal configuration file".format( - os.path.join(flag_default("config_dir"), "live"))) + os.path.join(flag_default("config_dir"), "live"))), + "usage": "\n\n certbot update_symlinks [options]\n\n" }), ] @@ -431,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] @@ -840,6 +849,12 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis helpful.add( 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", @@ -878,7 +893,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 " @@ -1076,24 +1091,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) @@ -1243,11 +1262,23 @@ def _plugins_parsing(helpful, plugins): helpful.add(["plugins", "certonly"], "--dns-dnsimple", action="store_true", help=('Obtain certificates using a DNS TXT record (if you are ' 'using DNSimple for DNS).')) + helpful.add(["plugins", "certonly"], "--dns-dnsmadeeasy", action="store_true", + help=('Obtain certificates using a DNS TXT record (if you are' + 'using DNS Made Easy for DNS).')) helpful.add(["plugins", "certonly"], "--dns-google", action="store_true", help=('Obtain certificates using a DNS TXT record (if you are ' 'using Google Cloud DNS).')) + helpful.add(["plugins", "certonly"], "--dns-luadns", action="store_true", + help=('Obtain certificates using a DNS TXT record (if you are ' + 'using LuaDNS for DNS).')) helpful.add(["plugins", "certonly"], "--dns-nsone", action="store_true", - help='Obtain certs using a DNS TXT record (if you are using NS1 for DNS).') + help=('Obtain certificates using a DNS TXT record (if you are ' + 'using NS1 for DNS).')) + helpful.add(["plugins", "certonly"], "--dns-rfc2136", action="store_true", + help='Obtain certificates using a DNS TXT record (if you are using BIND for DNS).') + helpful.add(["plugins", "certonly"], "--dns-route53", action="store_true", + help=('Obtain certificates using a DNS TXT record (if you are using Route53 for ' + 'DNS).')) # things should not be reorder past/pre this comment: # plugins_group should be displayed in --help before plugin @@ -1328,3 +1359,49 @@ def parse_preferred_challenges(pref_challs): raise errors.Error( "Unrecognized challenges: {0}".format(unrecognized)) return challs + + +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 af2868f6a..6010dd0a0 100644 --- a/certbot/client.py +++ b/certbot/client.py @@ -9,6 +9,7 @@ import OpenSSL import zope.component from acme import client as acme_client +from acme import crypto_util as acme_crypto_util from acme import errors as acme_errors from acme import jose from acme import messages @@ -53,6 +54,9 @@ def determine_user_agent(config): :rtype: `str` """ + # WARNING: To ensure changes are in line with Certbot's privacy + # 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} " "({5}; flags: {6}) Py/{7}") @@ -316,9 +320,17 @@ class Client(object): domains = [d for d in domains if d in auth_domains] # Create CSR from names - key = crypto_util.init_save_key( - self.config.rsa_key_size, self.config.key_dir) - csr = crypto_util.init_save_csr(key, domains, self.config.csr_dir) + if self.config.dry_run: + key = util.Key(file=None, + pem=crypto_util.make_key(self.config.rsa_key_size)) + csr = util.CSR(file=None, form="pem", + data=acme_crypto_util.make_csr( + key.pem, domains, self.config.must_staple)) + else: + key = crypto_util.init_save_key( + self.config.rsa_key_size, self.config.key_dir) + csr = crypto_util.init_save_csr(key, domains, self.config.csr_dir) + certr, chain = self.obtain_certificate_from_csr( domains, csr, authzr=authzr) diff --git a/certbot/constants.py b/certbot/constants.py index 7919b3317..dfdfcc0e8 100644 --- a/certbot/constants.py +++ b/certbot/constants.py @@ -18,7 +18,6 @@ CLI_DEFAULTS = dict( os.path.join(os.environ.get("XDG_CONFIG_HOME", "~/.config"), "letsencrypt", "cli.ini"), ], - dry_run=False, verbose_count=-int(logging.INFO / 10), server="https://acme-v01.api.letsencrypt.org/directory", rsa_key_size=2048, diff --git a/certbot/crypto_util.py b/certbot/crypto_util.py index 0ac0eee40..112ef7c85 100644 --- a/certbot/crypto_util.py +++ b/certbot/crypto_util.py @@ -55,15 +55,11 @@ def init_save_key(key_size, key_dir, keyname="key-certbot.pem"): # Save file util.make_or_verify_dir(key_dir, 0o700, os.geteuid(), config.strict_permissions) - if config.dry_run: - key_path = None - logger.debug("Generating key (%d bits), not saving to file", key_size) - else: - key_f, key_path = util.unique_file( - os.path.join(key_dir, keyname), 0o600, "wb") - with key_f: - key_f.write(key_pem) - logger.debug("Generating key (%d bits): %s", key_size, key_path) + key_f, key_path = util.unique_file( + os.path.join(key_dir, keyname), 0o600, "wb") + with key_f: + key_f.write(key_pem) + logger.debug("Generating key (%d bits): %s", key_size, key_path) return util.Key(key_path, key_pem) @@ -90,15 +86,11 @@ def init_save_csr(privkey, names, path): # Save CSR util.make_or_verify_dir(path, 0o755, os.geteuid(), config.strict_permissions) - if config.dry_run: - csr_filename = None - logger.debug("Creating CSR: not saving to file") - else: - csr_f, csr_filename = util.unique_file( - os.path.join(path, "csr-certbot.pem"), 0o644, "wb") - with csr_f: - csr_f.write(csr_pem) - logger.debug("Creating CSR: %s", csr_filename) + csr_f, csr_filename = util.unique_file( + os.path.join(path, "csr-certbot.pem"), 0o644, "wb") + with csr_f: + csr_f.write(csr_pem) + logger.debug("Creating CSR: %s", csr_filename) return util.CSR(csr_filename, csr_pem, "pem") @@ -222,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): @@ -246,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/display/util.py b/certbot/display/util.py index 5b01dd8d4..194b46a24 100644 --- a/certbot/display/util.py +++ b/certbot/display/util.py @@ -24,10 +24,10 @@ CANCEL = "cancel" """Display exit code for a user canceling the display.""" HELP = "help" -"""Display exit code when for when the user requests more help.""" +"""Display exit code when for when the user requests more help. (UNUSED)""" ESC = "esc" -"""Display exit code when the user hits Escape""" +"""Display exit code when the user hits Escape (UNUSED)""" def _wrap_lines(msg): @@ -123,8 +123,8 @@ class FileDisplay(object): else: logger.debug("Not pausing for user confirmation") - def menu(self, message, choices, ok_label="", cancel_label="", - help_label="", default=None, + def menu(self, message, choices, ok_label=None, cancel_label=None, + help_label=None, default=None, cli_flag=None, force_interactive=False, **unused_kwargs): # pylint: disable=unused-argument """Display a menu. @@ -176,12 +176,9 @@ class FileDisplay(object): if self._return_default(message, default, cli_flag, force_interactive): return OK, default - ans = input_with_timeout( - textwrap.fill( - "%s (Enter 'c' to cancel): " % message, - 80, - break_long_words=False, - break_on_hyphens=False)) + # Trailing space must be added outside of _wrap_lines to be preserved + message = _wrap_lines("%s (Enter 'c' to cancel):" % message) + " " + ans = input_with_timeout(message) if ans == "c" or ans == "C": return CANCEL, "-1" @@ -231,14 +228,13 @@ class FileDisplay(object): ans.startswith(no_label[0].upper())): return False - def checklist(self, message, tags, default_status=True, default=None, + def checklist(self, message, tags, default=None, cli_flag=None, force_interactive=False, **unused_kwargs): # pylint: disable=unused-argument """Display a checklist. :param str message: Message to display to user :param list tags: `str` tags to select, len(tags) > 0 - :param bool default_status: Not used for FileDisplay :param default: default value to return (if one exists) :param str cli_flag: option used to set this value with the CLI :param bool force_interactive: True if it's safe to prompt the user @@ -392,12 +388,8 @@ class FileDisplay(object): # Write out the menu choices for i, desc in enumerate(choices, 1): - self.outfile.write( - textwrap.fill( - "{num}: {desc}".format(num=i, desc=desc), - 80, - break_long_words=False, - break_on_hyphens=False)) + msg = "{num}: {desc}".format(num=i, desc=desc) + self.outfile.write(_wrap_lines(msg)) # Keep this outside of the textwrap self.outfile.write(os.linesep) 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/interfaces.py b/certbot/interfaces.py index f5c32f131..501a5c57e 100644 --- a/certbot/interfaces.py +++ b/certbot/interfaces.py @@ -396,8 +396,8 @@ class IDisplay(zope.interface.Interface): """ - def menu(message, choices, ok_label="OK", - cancel_label="Cancel", help_label="", + def menu(message, choices, ok_label=None, + cancel_label=None, help_label=None, default=None, cli_flag=None, force_interactive=False): """Displays a generic menu. @@ -409,9 +409,9 @@ class IDisplay(zope.interface.Interface): :param choices: choices :type choices: :class:`list` of :func:`tuple` or :class:`str` - :param str ok_label: label for OK button - :param str cancel_label: label for Cancel button - :param str help_label: label for Help button + :param str ok_label: label for OK button (UNUSED) + :param str cancel_label: label for Cancel button (UNUSED) + :param str help_label: label for Help button (UNUSED) :param int default: default (non-interactive) choice from the menu :param str cli_flag: to automate choice from the menu, eg "--keep" :param bool force_interactive: True if it's safe to prompt the user @@ -470,8 +470,7 @@ class IDisplay(zope.interface.Interface): """ - def checklist(message, tags, default_state, - default=None, cli_args=None, force_interactive=False): + def checklist(message, tags, default=None, cli_args=None, force_interactive=False): """Allow for multiple selections from a menu. When not setting force_interactive=True, you must provide a @@ -479,7 +478,6 @@ class IDisplay(zope.interface.Interface): :param str message: message to display to the user :param list tags: where each is of type :class:`str` len(tags) > 0 - :param bool default_status: If True, items are in a selected state by default. :param str default: default (non-interactive) state of the checklist :param str cli_flag: to automate choice from the menu, eg "--domains" :param bool force_interactive: True if it's safe to prompt the user 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 d3f6eaa09..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) @@ -161,7 +163,7 @@ def _handle_identical_cert_request(config, lineage): "Renew & replace the cert (limit ~5 per 7 days)"] display = zope.component.getUtility(interfaces.IDisplay) - response = display.menu(question, choices, "OK", "Cancel", + response = display.menu(question, choices, default=0, force_interactive=True) if response[0] == display_util.CANCEL: # TODO: Add notification related to command-line options for @@ -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/common.py b/certbot/plugins/common.py index aa58e86cc..255563bb6 100644 --- a/certbot/plugins/common.py +++ b/certbot/plugins/common.py @@ -241,6 +241,10 @@ class TLSSNI01(object): return os.path.join(self.configurator.config.work_dir, achall.chall.encode("token") + '.pem') + def get_z_domain(self, achall): + """Returns z_domain (SNI) name for the challenge.""" + return achall.response(achall.account_key).z_domain.decode("utf-8") + def _setup_challenge_cert(self, achall, cert_key=None): """Generate and write out challenge certificate.""" diff --git a/certbot/plugins/common_test.py b/certbot/plugins/common_test.py index 3eedf92f7..411cbe651 100644 --- a/certbot/plugins/common_test.py +++ b/certbot/plugins/common_test.py @@ -221,6 +221,11 @@ class TLSSNI01Test(unittest.TestCase): mock_safe_open.return_value.write.assert_called_once_with( OpenSSL.crypto.dump_privatekey(OpenSSL.crypto.FILETYPE_PEM, key)) + def test_get_z_domain(self): + achall = self.achalls[0] + self.assertEqual(self.sni.get_z_domain(achall), + achall.response(achall.account_key).z_domain.decode("utf-8")) + class InstallSslOptionsConfTest(test_util.TempDirTestCase): """Tests for certbot.plugins.common.install_ssl_options_conf.""" diff --git a/certbot/plugins/disco.py b/certbot/plugins/disco.py index af577564c..37baf98f7 100644 --- a/certbot/plugins/disco.py +++ b/certbot/plugins/disco.py @@ -32,8 +32,12 @@ class PluginEntryPoint(object): "certbot-dns-cloudxns", "certbot-dns-digitalocean", "certbot-dns-dnsimple", + "certbot-dns-dnsmadeeasy", "certbot-dns-google", + "certbot-dns-luadns", "certbot-dns-nsone", + "certbot-dns-rfc2136", + "certbot-dns-route53", "certbot-nginx", ] """Distributions for which prefix will be omitted.""" diff --git a/certbot/plugins/dns_common.py b/certbot/plugins/dns_common.py index f71905d79..ba88b7aef 100644 --- a/certbot/plugins/dns_common.py +++ b/certbot/plugins/dns_common.py @@ -139,7 +139,7 @@ class DNSAuthenticator(common.Plugin): setattr(self.config, self.dest(key), os.path.abspath(os.path.expanduser(new_value))) - def _configure_credentials(self, key, label, required_variables=None): + def _configure_credentials(self, key, label, required_variables=None, validator=None): """ As `_configure_file`, but for a credential configuration file. @@ -150,11 +150,20 @@ class DNSAuthenticator(common.Plugin): :param str key: The configuration key. :param str label: The user-friendly label for this piece of information. :param dict required_variables: Map of variable which must be present to error to display. + :param callable validator: A method which will be called to validate the + `CredentialsConfiguration` resulting from the supplied input after it has been validated + to contain the `required_variables`. Should throw a `~certbot.errors.PluginError` to + indicate any issue. """ def __validator(filename): + configuration = CredentialsConfiguration(filename, self.dest) + if required_variables: - CredentialsConfiguration(filename, self.dest).require(required_variables) + configuration.require(required_variables) + + if validator: + validator(configuration) self._configure_file(key, label, __validator) @@ -162,6 +171,9 @@ class DNSAuthenticator(common.Plugin): if required_variables: credentials_configuration.require(required_variables) + if validator: + validator(credentials_configuration) + return credentials_configuration @staticmethod diff --git a/certbot/plugins/manual.py b/certbot/plugins/manual.py index 25e2564fa..07371ad34 100644 --- a/certbot/plugins/manual.py +++ b/certbot/plugins/manual.py @@ -9,9 +9,38 @@ from acme import challenges from certbot import interfaces from certbot import errors from certbot import hooks +from certbot import reverter from certbot.plugins import common +class ManualTlsSni01(common.TLSSNI01): + """TLS-SNI-01 authenticator for the Manual plugin + + :ivar configurator: Authenticator object + :type configurator: :class:`~certbot.plugins.manual.Authenticator` + + :ivar list achalls: Annotated + class:`~certbot.achallenges.KeyAuthorizationAnnotatedChallenge` + challenges + + :param list indices: Meant to hold indices of challenges in a + larger array. NginxTlsSni01 is capable of solving many challenges + at once which causes an indexing issue within NginxConfigurator + who must return all responses in order. Imagine NginxConfigurator + maintaining state about where all of the http-01 Challenges, + TLS-SNI-01 Challenges belong in the response array. This is an + optional utility. + + :param str challenge_conf: location of the challenge config file + """ + + def perform(self): + """Create the SSL certificates and private keys""" + + for achall in self.achalls: + self._setup_challenge_cert(achall) + + @zope.interface.implementer(interfaces.IAuthenticator) @zope.interface.provider(interfaces.IPluginFactory) class Authenticator(common.Plugin): @@ -28,14 +57,18 @@ class Authenticator(common.Plugin): long_description = ( '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 ' - '$CERTBOT_AUTH_OUTPUT which contains the stdout output from the auth ' - 'script.') + 'The 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.') _DNS_INSTRUCTIONS = """\ Please deploy a DNS TXT record under the name {domain} with the following value: @@ -44,26 +77,29 @@ Please deploy a DNS TXT record under the name Before continuing, verify the record is deployed.""" _HTTP_INSTRUCTIONS = """\ -Make sure your web server displays the following content at -{uri} before continuing: +Create a file containing just this data: {validation} -If you don't have HTTP server configured, you can run the following -command on the target server (as root): +And make it available on your web server at this URL: -mkdir -p /tmp/certbot/public_html/{achall.URI_ROOT_PATH} -cd /tmp/certbot/public_html -printf "%s" {validation} > {achall.URI_ROOT_PATH}/{encoded_token} -# run only once per server: -$(command -v python2 || command -v python2.7 || command -v python2.6) -c \\ -"import BaseHTTPServer, SimpleHTTPServer; \\ -s = BaseHTTPServer.HTTPServer(('', {port}), SimpleHTTPServer.SimpleHTTPRequestHandler); \\ -s.serve_forever()" """ +{uri} +""" + _TLSSNI_INSTRUCTIONS = """\ +Configure the service listening on port {port} to present the certificate +{cert} +using the secret key +{key} +when it receives a TLS ClientHello with the SNI extension set to +{sni_domain} +""" def __init__(self, *args, **kwargs): super(Authenticator, self).__init__(*args, **kwargs) + self.reverter = reverter.Reverter(self.config) + self.reverter.recovery_routine() self.env = dict() + self.tls_sni_01 = None @classmethod def add_parser_arguments(cls, add): @@ -98,11 +134,10 @@ s.serve_forever()" """ def get_chall_pref(self, domain): # pylint: disable=missing-docstring,no-self-use,unused-argument - return [challenges.HTTP01, challenges.DNS01] + return [challenges.HTTP01, challenges.DNS01, challenges.TLSSNI01] def perform(self, achalls): # pylint: disable=missing-docstring self._verify_ip_logging_ok() - if self.conf('auth-hook'): perform_achall = self._perform_achall_with_script else: @@ -110,6 +145,12 @@ s.serve_forever()" """ responses = [] for achall in achalls: + if isinstance(achall.chall, challenges.TLSSNI01): + # Make a new ManualTlsSni01 instance for each challenge + # because the manual plugin deals with one challenge at a time. + self.tls_sni_01 = ManualTlsSni01(self) + self.tls_sni_01.add_chall(achall) + self.tls_sni_01.perform() perform_achall(achall) responses.append(achall.response(achall.account_key)) return responses @@ -135,6 +176,16 @@ s.serve_forever()" """ env['CERTBOT_TOKEN'] = achall.chall.encode('token') else: os.environ.pop('CERTBOT_TOKEN', None) + if isinstance(achall.chall, challenges.TLSSNI01): + env['CERTBOT_CERT_PATH'] = self.tls_sni_01.get_cert_path(achall) + env['CERTBOT_KEY_PATH'] = self.tls_sni_01.get_key_path(achall) + env['CERTBOT_SNI_DOMAIN'] = self.tls_sni_01.get_z_domain(achall) + os.environ.pop('CERTBOT_VALIDATION', None) + env.pop('CERTBOT_VALIDATION') + else: + os.environ.pop('CERTBOT_CERT_PATH', None) + os.environ.pop('CERTBOT_KEY_PATH', None) + os.environ.pop('CERTBOT_SNI_DOMAIN', None) os.environ.update(env) _, out = hooks.execute(self.conf('auth-hook')) env['CERTBOT_AUTH_OUTPUT'] = out.strip() @@ -147,11 +198,17 @@ s.serve_forever()" """ achall=achall, encoded_token=achall.chall.encode('token'), port=self.config.http01_port, uri=achall.chall.uri(achall.domain), validation=validation) - else: - assert isinstance(achall.chall, challenges.DNS01) + elif isinstance(achall.chall, challenges.DNS01): msg = self._DNS_INSTRUCTIONS.format( domain=achall.validation_domain_name(achall.domain), validation=validation) + else: + assert isinstance(achall.chall, challenges.TLSSNI01) + msg = self._TLSSNI_INSTRUCTIONS.format( + cert=self.tls_sni_01.get_cert_path(achall), + key=self.tls_sni_01.get_key_path(achall), + port=self.config.tls_sni_01_port, + sni_domain=self.tls_sni_01.get_z_domain(achall)) display = zope.component.getUtility(interfaces.IDisplay) display.notification(msg, wrap=False, force_interactive=True) @@ -163,3 +220,4 @@ s.serve_forever()" """ os.environ.pop('CERTBOT_TOKEN', None) os.environ.update(env) hooks.execute(self.conf('cleanup-hook')) + self.reverter.recovery_routine() diff --git a/certbot/plugins/manual_test.py b/certbot/plugins/manual_test.py index bd6816f02..ac528e81c 100644 --- a/certbot/plugins/manual_test.py +++ b/certbot/plugins/manual_test.py @@ -13,17 +13,31 @@ from certbot.tests import acme_util from certbot.tests import util as test_util -class AuthenticatorTest(unittest.TestCase): +class AuthenticatorTest(test_util.TempDirTestCase): """Tests for certbot.plugins.manual.Authenticator.""" def setUp(self): + super(AuthenticatorTest, self).setUp() self.http_achall = acme_util.HTTP01_A self.dns_achall = acme_util.DNS01_A - self.achalls = [self.http_achall, self.dns_achall] + self.tls_sni_achall = acme_util.TLSSNI01_A + self.achalls = [self.http_achall, self.dns_achall, self.tls_sni_achall] + for d in ["config_dir", "work_dir", "in_progress"]: + os.mkdir(os.path.join(self.tempdir, d)) + # "backup_dir" and "temp_checkpoint_dir" get created in + # certbot.util.make_or_verify_dir() during the Reverter + # initialization. self.config = mock.MagicMock( http01_port=0, manual_auth_hook=None, manual_cleanup_hook=None, manual_public_ip_logging_ok=False, noninteractive_mode=False, - validate_hooks=False) + validate_hooks=False, + config_dir=os.path.join(self.tempdir, "config_dir"), + work_dir=os.path.join(self.tempdir, "work_dir"), + backup_dir=os.path.join(self.tempdir, "backup_dir"), + temp_checkpoint_dir=os.path.join( + self.tempdir, "temp_checkpoint_dir"), + in_progress_dir=os.path.join(self.tempdir, "in_progess"), + tls_sni_01_port=5001) from certbot.plugins.manual import Authenticator self.auth = Authenticator(self.config, name='manual') @@ -42,7 +56,9 @@ class AuthenticatorTest(unittest.TestCase): def test_get_chall_pref(self): self.assertEqual(self.auth.get_chall_pref('example.org'), - [challenges.HTTP01, challenges.DNS01]) + [challenges.HTTP01, + challenges.DNS01, + challenges.TLSSNI01]) @test_util.patch_get_utility() def test_ip_logging_not_ok(self, mock_get_utility): @@ -58,13 +74,19 @@ class AuthenticatorTest(unittest.TestCase): def test_script_perform(self): self.config.manual_public_ip_logging_ok = True self.config.manual_auth_hook = ( - 'echo $CERTBOT_DOMAIN; echo ${CERTBOT_TOKEN:-notoken}; ' - 'echo $CERTBOT_VALIDATION;') - dns_expected = '{0}\n{1}\n{2}'.format( + 'echo ${CERTBOT_DOMAIN}; ' + 'echo ${CERTBOT_TOKEN:-notoken}; ' + 'echo ${CERTBOT_CERT_PATH:-nocert}; ' + 'echo ${CERTBOT_KEY_PATH:-nokey}; ' + 'echo ${CERTBOT_SNI_DOMAIN:-nosnidomain}; ' + 'echo ${CERTBOT_VALIDATION:-novalidation};') + dns_expected = '{0}\n{1}\n{2}\n{3}\n{4}\n{5}'.format( self.dns_achall.domain, 'notoken', + 'nocert', 'nokey', 'nosnidomain', self.dns_achall.validation(self.dns_achall.account_key)) - http_expected = '{0}\n{1}\n{2}'.format( + http_expected = '{0}\n{1}\n{2}\n{3}\n{4}\n{5}'.format( self.http_achall.domain, self.http_achall.chall.encode('token'), + 'nocert', 'nokey', 'nosnidomain', self.http_achall.validation(self.http_achall.account_key)) self.assertEqual( @@ -76,6 +98,17 @@ class AuthenticatorTest(unittest.TestCase): self.assertEqual( self.auth.env[self.http_achall.domain]['CERTBOT_AUTH_OUTPUT'], http_expected) + # tls_sni_01 challenge must be perform()ed above before we can + # get the cert_path and key_path. + tls_sni_expected = '{0}\n{1}\n{2}\n{3}\n{4}\n{5}'.format( + self.tls_sni_achall.domain, 'notoken', + self.auth.tls_sni_01.get_cert_path(self.tls_sni_achall), + self.auth.tls_sni_01.get_key_path(self.tls_sni_achall), + self.auth.tls_sni_01.get_z_domain(self.tls_sni_achall), + 'novalidation') + self.assertEqual( + self.auth.env[self.tls_sni_achall.domain]['CERTBOT_AUTH_OUTPUT'], + tls_sni_expected) @test_util.patch_get_utility() def test_manual_perform(self, mock_get_utility): @@ -85,7 +118,13 @@ class AuthenticatorTest(unittest.TestCase): [achall.response(achall.account_key) for achall in self.achalls]) for i, (args, kwargs) in enumerate(mock_get_utility().notification.call_args_list): achall = self.achalls[i] - self.assertTrue(achall.validation(achall.account_key) in args[0]) + if isinstance(achall.chall, challenges.TLSSNI01): + self.assertTrue( + self.auth.tls_sni_01.get_cert_path( + self.tls_sni_achall) in args[0]) + else: + self.assertTrue( + achall.validation(achall.account_key) in args[0]) self.assertFalse(kwargs['wrap']) def test_cleanup(self): @@ -98,16 +137,29 @@ class AuthenticatorTest(unittest.TestCase): self.auth.cleanup([achall]) self.assertEqual(os.environ['CERTBOT_AUTH_OUTPUT'], 'foo') self.assertEqual(os.environ['CERTBOT_DOMAIN'], achall.domain) - self.assertEqual( - os.environ['CERTBOT_VALIDATION'], - achall.validation(achall.account_key)) - + if (isinstance(achall.chall, challenges.HTTP01) or + isinstance(achall.chall, challenges.DNS01)): + self.assertEqual( + os.environ['CERTBOT_VALIDATION'], + achall.validation(achall.account_key)) if isinstance(achall.chall, challenges.HTTP01): self.assertEqual( os.environ['CERTBOT_TOKEN'], achall.chall.encode('token')) else: self.assertFalse('CERTBOT_TOKEN' in os.environ) + if isinstance(achall.chall, challenges.TLSSNI01): + self.assertEqual( + os.environ['CERTBOT_CERT_PATH'], + self.auth.tls_sni_01.get_cert_path(achall)) + self.assertEqual( + os.environ['CERTBOT_KEY_PATH'], + self.auth.tls_sni_01.get_key_path(achall)) + self.assertFalse( + os.path.exists(os.environ['CERTBOT_CERT_PATH'])) + self.assertFalse( + os.path.exists(os.environ['CERTBOT_KEY_PATH'])) + if __name__ == '__main__': diff --git a/certbot/plugins/selection.py b/certbot/plugins/selection.py index eb41d5b93..fdfbf2b15 100644 --- a/certbot/plugins/selection.py +++ b/certbot/plugins/selection.py @@ -112,7 +112,7 @@ def choose_plugin(prepared, question): while True: disp = z_util(interfaces.IDisplay) code, index = disp.menu( - question, opts, help_label="More Info", force_interactive=True) + question, opts, force_interactive=True) if code == display_util.OK: plugin_ep = prepared[index] @@ -123,18 +123,12 @@ def choose_plugin(prepared, question): "was:\n\n{0}".format(plugin_ep.prepare()), pause=False) else: return plugin_ep - elif code == display_util.HELP: - if prepared[index].misconfigured: - msg = "Reported Error: %s" % prepared[index].prepare() - else: - msg = prepared[index].init().more_info() - z_util(interfaces.IDisplay).notification(msg, - force_interactive=True) else: return None noninstaller_plugins = ["webroot", "manual", "standalone", "dns-cloudflare", "dns-cloudxns", - "dns-digitalocean", "dns-dnsimple", "dns-google", "dns-nsone"] + "dns-digitalocean", "dns-dnsimple", "dns-dnsmadeeasy", "dns-google", + "dns-luadns", "dns-nsone", "dns-rfc2136", "dns-route53"] def record_chosen_plugins(config, plugins, auth, inst): "Update the config entries to reflect the plugins we actually selected." @@ -216,7 +210,7 @@ def set_configurator(previously, now): return now -def cli_plugin_requests(config): +def cli_plugin_requests(config): # pylint: disable=too-many-branches """ Figure out which plugins the user requested with CLI and config options @@ -226,6 +220,7 @@ def cli_plugin_requests(config): req_inst = req_auth = config.configurator req_inst = set_configurator(req_inst, config.installer) req_auth = set_configurator(req_auth, config.authenticator) + if config.nginx: req_inst = set_configurator(req_inst, "nginx") req_auth = set_configurator(req_auth, "nginx") @@ -246,10 +241,18 @@ def cli_plugin_requests(config): req_auth = set_configurator(req_auth, "dns-digitalocean") if config.dns_dnsimple: req_auth = set_configurator(req_auth, "dns-dnsimple") + if config.dns_dnsmadeeasy: + req_auth = set_configurator(req_auth, "dns-dnsmadeeasy") if config.dns_google: req_auth = set_configurator(req_auth, "dns-google") + if config.dns_luadns: + req_auth = set_configurator(req_auth, "dns-luadns") if config.dns_nsone: req_auth = set_configurator(req_auth, "dns-nsone") + if config.dns_rfc2136: + req_auth = set_configurator(req_auth, "dns-rfc2136") + if config.dns_route53: + req_auth = set_configurator(req_auth, "dns-route53") logger.debug("Requested authenticator %s and installer %s", req_auth, req_inst) return req_auth, req_inst diff --git a/certbot/plugins/selection_test.py b/certbot/plugins/selection_test.py index 41c2b55c9..9f0716905 100644 --- a/certbot/plugins/selection_test.py +++ b/certbot/plugins/selection_test.py @@ -137,13 +137,10 @@ class ChoosePluginTest(unittest.TestCase): @test_util.patch_get_utility("certbot.plugins.selection.z_util") def test_more_info(self, mock_util): mock_util().menu.side_effect = [ - (display_util.HELP, 0), - (display_util.HELP, 1), (display_util.OK, 1), ] self.assertEqual(self.mock_stand, self._call()) - self.assertEqual(mock_util().notification.call_count, 2) @test_util.patch_get_utility("certbot.plugins.selection.z_util") def test_no_choice(self, mock_util): diff --git a/certbot/plugins/standalone.py b/certbot/plugins/standalone.py index ce878f84a..817403bd3 100644 --- a/certbot/plugins/standalone.py +++ b/certbot/plugins/standalone.py @@ -3,7 +3,6 @@ import argparse import collections import logging import socket -import threading import OpenSSL import six @@ -33,8 +32,6 @@ class ServerManager(object): will serve the same URLs! """ - _Instance = collections.namedtuple("_Instance", "server thread") - def __init__(self, certs, http_01_resources): self._instances = {} self.certs = certs @@ -51,34 +48,32 @@ class ServerManager(object): either `acme.challenge.HTTP01` or `acme.challenges.TLSSNI01`. :param str listenaddr: (optional) The address to listen on. Defaults to all addrs. - :returns: Server instance. + :returns: DualNetworkedServers instance. :rtype: ACMEServerMixin """ assert challenge_type in (challenges.TLSSNI01, challenges.HTTP01) if port in self._instances: - return self._instances[port].server + return self._instances[port] address = (listenaddr, port) try: if challenge_type is challenges.TLSSNI01: - server = acme_standalone.TLSSNI01Server(address, self.certs) + servers = acme_standalone.TLSSNI01DualNetworkedServers(address, self.certs) else: # challenges.HTTP01 - server = acme_standalone.HTTP01Server( + servers = acme_standalone.HTTP01DualNetworkedServers( address, self.http_01_resources) except socket.error as error: raise errors.StandaloneBindError(error, port) - thread = threading.Thread( - # pylint: disable=no-member - target=server.serve_forever) - thread.start() + servers.serve_forever() # if port == 0, then random free port on OS is taken # pylint: disable=no-member - real_port = server.socket.getsockname()[1] - self._instances[real_port] = self._Instance(server, thread) - return server + # both servers, if they exist, have the same port + real_port = servers.getsocknames()[0][1] + self._instances[real_port] = servers + return servers def stop(self, port): """Stop ACME server running on the specified ``port``. @@ -87,13 +82,12 @@ class ServerManager(object): """ instance = self._instances[port] - logger.debug("Stopping server at %s:%d...", - *instance.server.socket.getsockname()[:2]) - instance.server.shutdown() + for sockname in instance.getsocknames(): + logger.debug("Stopping server at %s:%d...", + *sockname[:2]) # Not calling server_close causes problems when renewing multiple # certs with `certbot renew` using TLSSNI01 and PyOpenSSL 0.13 - instance.server.server_close() - instance.thread.join() + instance.shutdown_and_server_close() del self._instances[port] def running(self): @@ -102,12 +96,11 @@ class ServerManager(object): Once the server is stopped using `stop`, it will not be returned. - :returns: Mapping from ``port`` to ``server``. + :returns: Mapping from ``port`` to ``servers``. :rtype: tuple """ - return dict((port, instance.server) for port, instance - in six.iteritems(self._instances)) + return self._instances.copy() SUPPORTED_CHALLENGES = [challenges.TLSSNI01, challenges.HTTP01] @@ -236,38 +229,38 @@ class Authenticator(common.Plugin): def _perform_single(self, achall): if isinstance(achall.chall, challenges.HTTP01): - server, response = self._perform_http_01(achall) + servers, response = self._perform_http_01(achall) else: # tls-sni-01 - server, response = self._perform_tls_sni_01(achall) - self.served[server].add(achall) + servers, response = self._perform_tls_sni_01(achall) + self.served[servers].add(achall) return response def _perform_http_01(self, achall): port = self.config.http01_port addr = self.config.http01_address - server = self.servers.run(port, challenges.HTTP01, listenaddr=addr) + servers = self.servers.run(port, challenges.HTTP01, listenaddr=addr) response, validation = achall.response_and_validation() resource = acme_standalone.HTTP01RequestHandler.HTTP01Resource( chall=achall.chall, response=response, validation=validation) self.http_01_resources.add(resource) - return server, response + return servers, response def _perform_tls_sni_01(self, achall): port = self.config.tls_sni_01_port addr = self.config.tls_sni_01_address - server = self.servers.run(port, challenges.TLSSNI01, listenaddr=addr) + servers = self.servers.run(port, challenges.TLSSNI01, listenaddr=addr) response, (cert, _) = achall.response_and_validation(cert_key=self.key) self.certs[response.z_domain] = (self.key, cert) - return server, response + return servers, response def cleanup(self, achalls): # pylint: disable=missing-docstring - # reduce self.served and close servers if none challenges are served - for server, server_achalls in self.served.items(): + # reduce self.served and close servers if no challenges are served + for unused_servers, server_achalls in self.served.items(): for achall in achalls: if achall in server_achalls: server_achalls.remove(achall) - for port, server in six.iteritems(self.servers.running()): - if not self.served[server]: + for port, servers in six.iteritems(self.servers.running()): + if not self.served[servers]: self.servers.stop(port) diff --git a/certbot/plugins/standalone_test.py b/certbot/plugins/standalone_test.py index 65d16c2f2..2a55c516f 100644 --- a/certbot/plugins/standalone_test.py +++ b/certbot/plugins/standalone_test.py @@ -32,7 +32,7 @@ class ServerManagerTest(unittest.TestCase): def _test_run_stop(self, challenge_type): server = self.mgr.run(port=0, challenge_type=challenge_type) - port = server.socket.getsockname()[1] # pylint: disable=no-member + port = server.getsocknames()[0][1] # pylint: disable=no-member self.assertEqual(self.mgr.running(), {port: server}) self.mgr.stop(port=port) self.assertEqual(self.mgr.running(), {}) @@ -45,7 +45,7 @@ class ServerManagerTest(unittest.TestCase): def test_run_idempotent(self): server = self.mgr.run(port=0, challenge_type=challenges.HTTP01) - port = server.socket.getsockname()[1] # pylint: disable=no-member + port = server.getsocknames()[0][1] # pylint: disable=no-member server2 = self.mgr.run(port=port, challenge_type=challenges.HTTP01) self.assertEqual(self.mgr.running(), {port: server}) self.assertTrue(server is server2) @@ -53,9 +53,14 @@ class ServerManagerTest(unittest.TestCase): self.assertEqual(self.mgr.running(), {}) def test_run_bind_error(self): - some_server = socket.socket() + some_server = socket.socket(socket.AF_INET6) some_server.bind(("", 0)) port = some_server.getsockname()[1] + maybe_another_server = socket.socket() + try: + maybe_another_server.bind(("", port)) + except socket.error: + pass self.assertRaises( errors.StandaloneBindError, self.mgr.run, port, challenge_type=challenges.HTTP01) diff --git a/certbot/plugins/webroot.py b/certbot/plugins/webroot.py index aad6ffc82..4662b2aa6 100644 --- a/certbot/plugins/webroot.py +++ b/certbot/plugins/webroot.py @@ -117,22 +117,11 @@ to serve all files under specified web root ({0}).""" code, index = display.menu( "Select the webroot for {0}:".format(domain), ["Enter a new webroot"] + known_webroots, - help_label="Help", cli_flag=path_flag, force_interactive=True) + cli_flag=path_flag, force_interactive=True) if code == display_util.CANCEL: raise errors.PluginError( "Every requested domain must have a " "webroot when using the webroot plugin.") - elif code == display_util.HELP: - display.notification( - "To use the webroot plugin, you need to have an " - "HTTP server running on this system serving files " - "for the requested domain. Additionally, this " - "server should be serving all files contained in a " - "public_html or webroot directory. The webroot " - "plugin works by temporarily saving necessary " - "resources in the HTTP server's webroot directory " - "to pass domain validation challenges.", - force_interactive=True) else: # code == display_util.OK return None if index == 0 else known_webroots[index - 1] @@ -141,10 +130,7 @@ to serve all files under specified web root ({0}).""" _validate_webroot, "Input the webroot for {0}:".format(domain), force_interactive=True) - if code == display_util.HELP: - # Displaying help is not currently implemented - return None - elif code == display_util.CANCEL or code == display_util.ESC: + if code == display_util.CANCEL: return None else: # code == display_util.OK return _validate_webroot(webroot) diff --git a/certbot/plugins/webroot_test.py b/certbot/plugins/webroot_test.py index 53809764b..e202a0a6d 100644 --- a/certbot/plugins/webroot_test.py +++ b/certbot/plugins/webroot_test.py @@ -84,10 +84,8 @@ class AuthenticatorTest(unittest.TestCase): self.config.webroot_map = {"otherthing.com": self.path} mock_display = mock_get_utility() - mock_display.menu.side_effect = ((display_util.HELP, -1), - (display_util.CANCEL, -1),) + mock_display.menu.side_effect = ((display_util.CANCEL, -1),) self.assertRaises(errors.PluginError, self.auth.perform, [self.achall]) - self.assertTrue(mock_display.notification.called) self.assertTrue(mock_display.menu.called) for call in mock_display.menu.call_args_list: self.assertTrue(self.achall.domain in call[0][0]) @@ -103,8 +101,7 @@ class AuthenticatorTest(unittest.TestCase): mock_display = mock_get_utility() mock_display.menu.return_value = (display_util.OK, 0,) with mock.patch('certbot.display.ops.validated_directory') as m: - m.side_effect = ((display_util.HELP, -1), - (display_util.CANCEL, -1), + m.side_effect = ((display_util.CANCEL, -1), (display_util.OK, self.path,)) self.auth.perform([self.achall]) 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/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 391a407ac..97fd6241d 100644 --- a/certbot/tests/client_test.py +++ b/certbot/tests/client_test.py @@ -144,6 +144,7 @@ class ClientTest(ClientTestCommon): 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"] def test_init_acme_verify_ssl(self): @@ -241,15 +242,37 @@ class ClientTest(ClientTestCommon): self.assertEqual(1, mock_get_utility().notification.call_count) @mock.patch("certbot.client.crypto_util") - @test_util.patch_get_utility() - def test_obtain_certificate(self, unused_mock_get_utility, - mock_crypto_util): - self._mock_obtain_certificate() - + def test_obtain_certificate(self, mock_crypto_util): csr = util.CSR(form="pem", file=None, data=CSR_SAN) mock_crypto_util.init_save_csr.return_value = csr mock_crypto_util.init_save_key.return_value = mock.sentinel.key - domains = ["example.com", "www.example.com"] + + self._test_obtain_certificate_common(mock.sentinel.key, csr) + + mock_crypto_util.init_save_key.assert_called_once_with( + self.config.rsa_key_size, self.config.key_dir) + mock_crypto_util.init_save_csr.assert_called_once_with( + mock.sentinel.key, self.eg_domains, self.config.csr_dir) + + @mock.patch("certbot.client.crypto_util") + @mock.patch("certbot.client.acme_crypto_util") + def test_obtain_certificate_dry_run(self, mock_acme_crypto, mock_crypto): + csr = util.CSR(form="pem", file=None, data=CSR_SAN) + mock_acme_crypto.make_csr.return_value = CSR_SAN + 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) + + mock_crypto.make_key.assert_called_once_with(self.config.rsa_key_size) + mock_acme_crypto.make_csr.assert_called_once_with( + mock.sentinel.key_pem, self.eg_domains, self.config.must_staple) + mock_crypto.init_save_key.assert_not_called() + mock_crypto.init_save_csr.assert_not_called() + + def _test_obtain_certificate_common(self, key, csr): + self._mock_obtain_certificate() # return_value is essentially set to (None, None) in # _mock_obtain_certificate(), which breaks this test. @@ -258,7 +281,7 @@ class ClientTest(ClientTestCommon): authzr = [] # domain ordering should not be affected by authorization order - for domain in reversed(domains): + for domain in reversed(self.eg_domains): authzr.append( mock.MagicMock( body=mock.MagicMock( @@ -267,14 +290,12 @@ class ClientTest(ClientTestCommon): self.client.auth_handler.get_authorizations.return_value = authzr - self.assertEqual( - self.client.obtain_certificate(domains), - (mock.sentinel.certr, mock.sentinel.chain, mock.sentinel.key, csr)) + with test_util.patch_get_utility(): + result = self.client.obtain_certificate(self.eg_domains) - mock_crypto_util.init_save_key.assert_called_once_with( - self.config.rsa_key_size, self.config.key_dir) - mock_crypto_util.init_save_csr.assert_called_once_with( - mock.sentinel.key, domains, self.config.csr_dir) + self.assertEqual( + result, + (mock.sentinel.certr, mock.sentinel.chain, key, csr)) self._check_obtain_certificate() @mock.patch('certbot.client.Client.obtain_certificate') diff --git a/certbot/tests/crypto_util_test.py b/certbot/tests/crypto_util_test.py index ea5a81062..bc41ca950 100644 --- a/certbot/tests/crypto_util_test.py +++ b/certbot/tests/crypto_util_test.py @@ -30,8 +30,7 @@ class InitSaveKeyTest(test_util.TempDirTestCase): logging.disable(logging.CRITICAL) zope.component.provideUtility( - mock.Mock(strict_permissions=True, dry_run=False), - interfaces.IConfig) + mock.Mock(strict_permissions=True), interfaces.IConfig) def tearDown(self): super(InitSaveKeyTest, self).tearDown() @@ -51,16 +50,6 @@ class InitSaveKeyTest(test_util.TempDirTestCase): self.assertTrue('key-certbot.pem' in key.file) self.assertTrue(os.path.exists(os.path.join(self.tempdir, key.file))) - @mock.patch('certbot.crypto_util.make_key') - def test_success_dry_run(self, mock_make): - zope.component.provideUtility( - mock.Mock(strict_permissions=True, dry_run=True), - interfaces.IConfig) - mock_make.return_value = b'key_pem' - key = self._call(1024, self.tempdir) - self.assertEqual(key.pem, b'key_pem') - self.assertTrue(key.file is None) - @mock.patch('certbot.crypto_util.make_key') def test_key_failure(self, mock_make): mock_make.side_effect = ValueError @@ -74,12 +63,11 @@ class InitSaveCSRTest(test_util.TempDirTestCase): super(InitSaveCSRTest, self).setUp() zope.component.provideUtility( - mock.Mock(strict_permissions=True, dry_run=False), - interfaces.IConfig) + mock.Mock(strict_permissions=True), interfaces.IConfig) @mock.patch('acme.crypto_util.make_csr') @mock.patch('certbot.crypto_util.util.make_or_verify_dir') - def test_success(self, unused_mock_verify, mock_csr): + def test_it(self, unused_mock_verify, mock_csr): from certbot.crypto_util import init_save_csr mock_csr.return_value = b'csr_pem' @@ -90,22 +78,6 @@ class InitSaveCSRTest(test_util.TempDirTestCase): self.assertEqual(csr.data, b'csr_pem') self.assertTrue('csr-certbot.pem' in csr.file) - @mock.patch('acme.crypto_util.make_csr') - @mock.patch('certbot.crypto_util.util.make_or_verify_dir') - def test_success_dry_run(self, unused_mock_verify, mock_csr): - from certbot.crypto_util import init_save_csr - - zope.component.provideUtility( - mock.Mock(strict_permissions=True, dry_run=True), - interfaces.IConfig) - mock_csr.return_value = b'csr_pem' - - csr = init_save_csr( - mock.Mock(pem='dummy_key'), 'example.com', self.tempdir) - - self.assertEqual(csr.data, b'csr_pem') - self.assertTrue(csr.file is None) - class ValidCSRTest(unittest.TestCase): """Tests for certbot.crypto_util.valid_csr.""" @@ -280,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/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..5ee9ad812 100644 --- a/certbot/tests/log_test.py +++ b/certbot/tests/log_test.py @@ -66,8 +66,8 @@ 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']) + debug=False, logs_dir=self.tempdir, max_log_backups=1000, + quiet=False, verbose_count=constants.CLI_DEFAULTS['verbose_count']) self.devnull = open(os.devnull, 'w') from certbot.log import ColoredStreamHandler @@ -129,7 +129,7 @@ class SetupLogFileHandlerTest(test_util.TempDirTestCase): def setUp(self): super(SetupLogFileHandlerTest, self).setUp() - self.config = mock.MagicMock(logs_dir=self.tempdir) + self.config = mock.MagicMock(logs_dir=self.tempdir, max_log_backups=42) @mock.patch('certbot.main.logging.handlers.RotatingFileHandler') def test_failure(self, mock_handler): @@ -142,15 +142,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..48e19b7b3 100644 --- a/certbot/tests/main_test.py +++ b/certbot/tests/main_test.py @@ -37,6 +37,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): @@ -678,11 +680,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 +694,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]) @@ -1000,6 +1004,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 +1017,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/util.py b/certbot/tests/util.py index 76e3d5846..a36f0f6ac 100644 --- a/certbot/tests/util.py +++ b/certbot/tests/util.py @@ -276,7 +276,7 @@ def lock_and_call(func, lock_path): def hold_lock(cv, lock_path): # pragma: no cover """Acquire a file lock at lock_path and wait to release it. - :param multiprocessing.Condition cv: condition for syncronization + :param multiprocessing.Condition cv: condition for synchronization :param str lock_path: path to the file lock """ diff --git a/certbot/tests/util_test.py b/certbot/tests/util_test.py index 3f6bd2a39..7e320012a 100644 --- a/certbot/tests/util_test.py +++ b/certbot/tests/util_test.py @@ -420,6 +420,13 @@ class EnforceLeValidity(unittest.TestCase): def test_valid_domain(self): self.assertEqual(self._call(u"example.com"), u"example.com") + def test_input_with_scheme(self): + self.assertRaises(errors.ConfigurationError, self._call, u"http://example.com") + self.assertRaises(errors.ConfigurationError, self._call, u"https://example.com") + + def test_valid_input_with_scheme_name(self): + self.assertEqual(self._call(u"http.example.com"), u"http.example.com") + class EnforceDomainSanityTest(unittest.TestCase): """Test enforce_domain_sanity.""" diff --git a/certbot/util.py b/certbot/util.py index 041515199..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.")) @@ -568,6 +568,17 @@ def enforce_domain_sanity(domain): # Remove trailing dot domain = domain[:-1] if domain.endswith(u'.') else domain + # Separately check for odd "domains" like "http://example.com" to fail + # fast and provide a clear error message + for scheme in ["http", "https"]: # Other schemes seem unlikely + if domain.startswith("{0}://".format(scheme)): + raise errors.ConfigurationError( + "Requested name {0} appears to be a URL, not a FQDN. " + "Try again without the leading \"{1}://\".".format( + domain, scheme + ) + ) + # Explain separately that IP addresses aren't allowed (apart from not # being FQDNs) because hope springs eternal concerning this point try: 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/cli-help.txt b/docs/cli-help.txt index 1c46ea2c3..6ff284788 100644 --- a/docs/cli-help.txt +++ b/docs/cli-help.txt @@ -3,26 +3,26 @@ usage: Certbot can obtain and install HTTPS/TLS/SSL certificates. By default, it will attempt to use a webserver both for obtaining and installing the -cert. The most common SUBCOMMANDS and flags are: +certificate. The most common SUBCOMMANDS and flags are: obtain, install, and renew certificates: - (default) run Obtain & install a cert in your current webserver - certonly Obtain or renew a cert, but do not install it - renew Renew all previously obtained certs that are near expiry - -d DOMAINS Comma-separated list of domains to obtain a cert for + (default) run Obtain & install a certificate in your current webserver + certonly Obtain or renew a certificate, but do not install it + renew Renew all previously obtained certificates that are near expiry + -d DOMAINS Comma-separated list of domains to obtain a certificate for --apache Use the Apache plugin for authentication & installation --standalone Run a standalone webserver for authentication --nginx Use the Nginx plugin for authentication & installation --webroot Place files in a server's webroot folder for authentication - --manual Obtain certs interactively, or using shell script hooks + --manual Obtain certificates interactively, or using shell script hooks -n Run non-interactively - --test-cert Obtain a test cert from a staging server - --dry-run Test "renew" or "certonly" without saving any certs to disk + --test-cert Obtain a test certificate from a staging server + --dry-run Test "renew" or "certonly" without saving any certificates to disk manage certificates: - certificates Display information about certs you have from Certbot + certificates Display information about certificates you have from Certbot revoke Revoke a certificate (supply --cert-path) delete Delete a certificate @@ -57,19 +57,19 @@ optional arguments: certificate, specifies the new certificate's name. (default: None) --dry-run Perform a test run of the client, obtaining test - (invalid) certs but not saving them to disk. This can - currently only be used with the 'certonly' and 'renew' - subcommands. Note: Although --dry-run tries to avoid - making any persistent changes on a system, it is not - completely side-effect free: if used with webserver - authenticator plugins like apache and nginx, it makes - and then reverts temporary config changes in order to - obtain test certs, and reloads webservers to deploy - and then roll back those changes. It also calls --pre- - hook and --post-hook commands if they are defined - because they may be necessary to accurately simulate - renewal. --renew-hook commands are not called. - (default: False) + (invalid) certificates but not saving them to disk. + This can currently only be used with the 'certonly' + and 'renew' subcommands. Note: Although --dry-run + tries to avoid making any persistent changes on a + system, it is not completely side-effect free: if used + with webserver authenticator plugins like apache and + nginx, it makes and then reverts temporary config + changes in order to obtain test 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. (default: False) --debug-challenges After setting up challenges, wait for user input before submitting to CA (default: False) --preferred-challenges PREF_CHALLS @@ -89,7 +89,7 @@ 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.14.2 (certbot; + "". (default: CertbotACMEClient/0.16.0 (certbot; Ubuntu 16.04.2 LTS) Authenticator/XXX Installer/YYY (SUBCOMMAND; flags: FLAGS) Py/2.7.12). The flags encoded in the user agent are: --duplicate, --force- @@ -100,11 +100,11 @@ automation: Arguments for automating execution & other tweaks --keep-until-expiring, --keep, --reinstall - If the requested cert matches an existing cert, always - keep the existing one until it is due for renewal (for - the 'run' subcommand this means reinstall the existing - cert). (default: Ask) - --expand If an existing cert is a strict subset of the + If the requested certificate matches an existing + certificate, always keep the existing one until it is + due for renewal (for the 'run' subcommand this means + reinstall the existing certificate). (default: Ask) + --expand If an existing certificate is a strict subset of the requested names, always expand and replace it with the additional names. (default: Ask) --version show program's version number and exit @@ -176,8 +176,9 @@ testing: --test-cert, --staging Use the staging server to obtain or revoke test - (invalid) certs; equivalent to --server https://acme- - staging.api.letsencrypt.org/directory (default: False) + (invalid) certificates; equivalent to --server https + ://acme-staging.api.letsencrypt.org/directory + (default: False) --debug Show tracebacks in case of errors, and allow certbot- auto execution on experimental platforms (default: False) @@ -188,25 +189,32 @@ testing: affects the port Certbot listens on. A conforming ACME server will still attempt to connect on port 443. (default: 443) + --tls-sni-01-address TLS_SNI_01_ADDRESS + The address the server listens to during tls-sni-01 + challenge. (default: ) --http-01-port HTTP01_PORT Port used in the http-01 challenge. This only affects the port Certbot listens on. A conforming ACME server will still attempt to connect on port 80. (default: 80) - --break-my-certs Be willing to replace or renew valid certs with - invalid (testing/staging) certs (default: False) + --http-01-address HTTP01_ADDRESS + The address the server listens to during http-01 + challenge. (default: ) + --break-my-certs Be willing to replace or renew valid certificates with + invalid (testing/staging) certificates (default: + False) paths: Arguments changing execution paths & servers --cert-path CERT_PATH - Path to where cert is saved (with auth --csr), + Path to where certificate is saved (with auth --csr), installed from, or revoked. (default: None) - --key-path KEY_PATH Path to private key for cert installation or + --key-path KEY_PATH Path to private key for certificate installation or revocation (if account key is missing) (default: None) --fullchain-path FULLCHAIN_PATH - Accompanying path to a full certificate chain (cert - plus chain). (default: None) + Accompanying path to a full certificate chain + (certificate plus chain). (default: None) --chain-path CHAIN_PATH Accompanying path to a certificate chain. (default: None) @@ -230,10 +238,10 @@ manage: directory run: - Options for obtaining & installing certs + Options for obtaining & installing certificates certonly: - Options for modifying how a cert is obtained + Options for modifying how a certificate is obtained --csr CSR Path to a Certificate Signing Request (CSR) in DER or PEM format. Currently --csr only works with the @@ -272,10 +280,10 @@ renew: the shell variable $RENEWED_LINEAGE will point to the config live subdirectory (for example, "/etc/letsencrypt/live/example.com") containing the - new certs and keys; the shell variable + new certificates and keys; the shell variable $RENEWED_DOMAINS will contain a space-delimited list - of renewed cert domains (for example, "example.com - www.example.com" (default: None) + of renewed certificate domains (for example, + "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 @@ -293,7 +301,7 @@ delete: Options for deleting a certificate revoke: - Options for revocation of certs + Options for revocation of certificates --reason {keycompromise,affiliationchanged,superseded,unspecified,cessationofoperation} Specify reason for revoking certificate. (default: 0) @@ -329,7 +337,7 @@ unregister: --account ACCOUNT_ID Account ID to use (default: None) install: - Options for modifying how a cert is deployed + Options for modifying how a certificate is deployed config_changes: Options for controlling which changes are displayed @@ -352,8 +360,8 @@ plugins: --installers Limit to installer plugins only. (default: None) update_symlinks: - Recreates cert and key symlinks in /etc/letsencrypt/live, if you changed - them by hand or edited a renewal configuration file + Recreates certificate and key symlinks in /etc/letsencrypt/live, if you + changed them by hand or edited a renewal configuration file plugins: Plugin Selection: Certbot client supports an extensible plugins @@ -371,14 +379,36 @@ plugins: -i INSTALLER, --installer INSTALLER Installer plugin name (also used to find domains). (default: None) - --apache Obtain and install certs using Apache (default: False) - --nginx Obtain and install certs using Nginx (default: False) - --standalone Obtain certs using a "standalone" webserver. (default: + --apache Obtain and install certificates using Apache (default: False) - --manual Provide laborious manual instructions for obtaining a - cert (default: False) - --webroot Obtain certs by placing files in a webroot directory. + --nginx Obtain and install certificates using Nginx (default: + False) + --standalone Obtain certificates using a "standalone" webserver. (default: False) + --manual Provide laborious manual instructions for obtaining a + certificate (default: False) + --webroot Obtain certificates by placing files in a webroot + directory. (default: False) + --dns-cloudflare Obtain certificates using a DNS TXT record (if you are + using Cloudflare for DNS). (default: False) + --dns-cloudxns Obtain certificates using a DNS TXT record (if you are + using CloudXNS for DNS). (default: False) + --dns-digitalocean Obtain certificates using a DNS TXT record (if you are + 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 - Beta @@ -410,14 +440,141 @@ apache: Let installer handle enabling sites for you.(Only Ubuntu/Debian currently) (default: True) +certbot-route53:auth: + Obtain certificates using a DNS TXT record (if you are using AWS Route53 + for DNS). + + --certbot-route53:auth-propagation-seconds CERTBOT_ROUTE53:AUTH_PROPAGATION_SECONDS + The number of seconds to wait for DNS to propagate + before asking the ACME server to verify the DNS + record. (default: 10) + +dns-cloudflare: + Obtain certificates using a DNS TXT record (if you are using Cloudflare + for DNS). + + --dns-cloudflare-propagation-seconds DNS_CLOUDFLARE_PROPAGATION_SECONDS + The number of seconds to wait for DNS to propagate + before asking the ACME server to verify the DNS + record. (default: 10) + --dns-cloudflare-credentials DNS_CLOUDFLARE_CREDENTIALS + Cloudflare credentials INI file. (default: None) + +dns-cloudxns: + Obtain certificates using a DNS TXT record (if you are using CloudXNS for + DNS). + + --dns-cloudxns-propagation-seconds DNS_CLOUDXNS_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-cloudxns-credentials DNS_CLOUDXNS_CREDENTIALS + CloudXNS credentials INI file. (default: None) + +dns-digitalocean: + Obtain certs using a DNS TXT record (if you are using DigitalOcean for + DNS). + + --dns-digitalocean-propagation-seconds DNS_DIGITALOCEAN_PROPAGATION_SECONDS + The number of seconds to wait for DNS to propagate + before asking the ACME server to verify the DNS + record. (default: 10) + --dns-digitalocean-credentials DNS_DIGITALOCEAN_CREDENTIALS + DigitalOcean credentials INI file. (default: None) + +dns-dnsimple: + Obtain certificates using a DNS TXT record (if you are using DNSimple for + DNS). + + --dns-dnsimple-propagation-seconds DNS_DNSIMPLE_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-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). + + --dns-google-propagation-seconds DNS_GOOGLE_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-google-credentials DNS_GOOGLE_CREDENTIALS + Path to Google Cloud DNS service account JSON file. + (See https://developers.google.com/identity/protocols/ + OAuth2ServiceAccount#creatinganaccount forinformation + about creating a service account and + https://cloud.google.com/dns/access- + 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). + + --dns-nsone-propagation-seconds DNS_NSONE_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-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). + + --dns-route53-propagation-seconds DNS_ROUTE53_PROPAGATION_SECONDS + The number of seconds to wait for DNS to propagate + before asking the ACME server to verify the DNS + record. (default: 10) + 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. diff --git a/docs/index.rst b/docs/index.rst index 9caf2b0fd..17cde1adf 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -17,12 +17,6 @@ Welcome to the Certbot documentation! api -.. toctree:: - :hidden: - - challenges - ciphers - Indices and tables ================== diff --git a/docs/install.rst b/docs/install.rst index a1e91c010..e75a8b3e2 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 diff --git a/docs/packaging.rst b/docs/packaging.rst index 3b7e21ab3..3d58ea92e 100644 --- a/docs/packaging.rst +++ b/docs/packaging.rst @@ -11,6 +11,16 @@ We release packages and upload them to PyPI (wheels and source tarballs). - https://pypi.python.org/pypi/certbot - https://pypi.python.org/pypi/certbot-apache - https://pypi.python.org/pypi/certbot-nginx +- https://pypi.python.org/pypi/certbot-dns-cloudflare +- 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: @@ -43,10 +53,20 @@ Arch From our official releases: -- https://www.archlinux.org/packages/community/any/python2-acme +- https://www.archlinux.org/packages/community/any/python-acme - https://www.archlinux.org/packages/community/any/certbot - https://www.archlinux.org/packages/community/any/certbot-apache - https://www.archlinux.org/packages/community/any/certbot-nginx +- https://www.archlinux.org/packages/community/any/certbot-dns-cloudflare +- 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 0582c804b..ef81f400e 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) @@ -49,14 +49,14 @@ apache_ Y Y | Automates obtaining and installing a certificate with Ap webroot_ Y N | Obtains a certificate by writing to the webroot directory of http-01_ (80) | an already running webserver. nginx_ Y Y | Automates obtaining and installing a certificate with Nginx. tls-sni-01_ (443) - | Alpha release shipped with Certbot 0.9.0. + | Shipped with Certbot 0.9.0. standalone_ Y N | Uses a "standalone" webserver to obtain a certificate. http-01_ (80) or | Requires port 80 or 443 to be available. This is useful on tls-sni-01_ (443) | systems with no webserver, or when direct integration with | the local webserver is not supported or not desired. -manual_ Y N | Helps you obtain a certificate by giving you instructions to http-01_ (80) or - | perform domain validation yourself. Additionally allows you dns-01_ (53) - | to specify scripts to automate the validation task in a +manual_ Y N | Helps you obtain a certificate by giving you instructions to http-01_ (80), + | perform domain validation yourself. Additionally allows you dns-01_ (53) or + | to specify scripts to automate the validation task in a tls-sni-01_ (443) | customized way. =========== ==== ==== =============================================================== ============================= @@ -132,7 +132,7 @@ Nginx ----- The Nginx plugin has been distributed with Certbot since version 0.9.0 and should -work for most configurations. Because it is alpha code, we recommend backing up Nginx +work for most configurations. We recommend backing up Nginx configurations before using it (though you can also revert changes to configurations with ``certbot --nginx rollback``). You can use it by providing the ``--nginx`` flag on the commandline. @@ -175,13 +175,15 @@ the UI, you can use the plugin to obtain a certificate by specifying to copy and paste commands into another terminal session, which may be on a different computer. -The manual plugin can use either the ``http`` or the ``dns`` challenge. You -can use the ``--preferred-challenges`` option to choose the challenge of your -preference. +The manual plugin can use either the ``http``, ``dns`` or the +``tls-sni`` challenge. You can use the ``--preferred-challenges`` option +to choose the challenge of your preference. + The ``http`` challenge will ask you to place a file with a specific name and specific content in the ``/.well-known/acme-challenge/`` directory directly in the top-level directory (“web root”) containing the files served by your webserver. In essence it's the same as the webroot_ plugin, but not automated. + When using the ``dns`` challenge, ``certbot`` will ask you to place a TXT DNS record with specific contents under the domain name consisting of the hostname for which you want a certificate issued, prepended by ``_acme-challenge``. @@ -192,10 +194,36 @@ For example, for the domain ``example.com``, a zone file entry would look like: _acme-challenge.example.com. 300 IN TXT "gfj9Xq...Rg85nM" -Additionally you can specify scripts to prepare for validation and 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. +When using the ``tls-sni`` challenge, ``certbot`` will prepare a self-signed +SSL certificate for you with the challenge validation appropriately +encoded into a subjectAlternatNames entry. You will need to configure +your SSL server to present this challenge SSL certificate to the ACME +server using SNI. + +Additionally you can specify scripts to prepare for validation and +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: @@ -409,15 +437,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 @@ -450,7 +478,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 @@ -516,7 +544,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). @@ -610,7 +638,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: @@ -626,12 +654,15 @@ and ``--manual-cleanup-hook`` respectively and can be used as follows: certbot certonly --manual --manual-auth-hook /path/to/http/authenticator.sh --manual-cleanup-hook /path/to/http/cleanup.sh -d secure.example.com This will run the ``authenticator.sh`` script, attempt the validation, and then run -the ``cleanup.sh`` script. Additionally certbot will pass three environment +the ``cleanup.sh`` script. Additionally certbot will pass relevant environment variables to these scripts: - ``CERTBOT_DOMAIN``: The domain being authenticated -- ``CERTBOT_VALIDATION``: The validation string +- ``CERTBOT_VALIDATION``: The validation string (HTTP-01 and DNS-01 only) - ``CERTBOT_TOKEN``: Resource name part of the HTTP-01 challenge (HTTP-01 only) +- ``CERTBOT_CERT_PATH``: The challenge SSL certificate (TLS-SNI-01 only) +- ``CERTBOT_KEY_PATH``: The private key associated with the aforementioned SSL certificate (TLS-SNI-01 only) +- ``CERTBOT_SNI_DOMAIN``: The SNI name for which the ACME server expects to be presented the self-signed certificate located at ``$CERTBOT_CERT_PATH`` (TLS-SNI-01 only) Additionally for cleanup: @@ -731,7 +762,28 @@ Example usage for DNS-01 (Cloudflare API v4) (for example purposes only, do not fi fi +.. _lock-files: +Lock Files +========== + +When processing a validation Certbot writes a number of lock files on your system +to prevent multiple instances from overwriting each other's changes. This means +that be default two instances of Certbot will not be able to run in parallel. + +Since the directories used by Certbot are configurable, Certbot +will write a lock file for all of the directories it uses. This include Certbot's +``--work-dir``, ``--logs-dir``, and ``--config-dir``. By default these are +``/var/lib/letsencrypt``, ``/var/logs/letsencrypt``, and ``/etc/letsencrypt`` +respectively. Additionally if you are using Certbot with Apache or nginx it will +lock the configuration folder for that program, which are typically also in the +``/etc`` directory. + +Note that these lock files will only prevent other instances of Certbot from +using those directories, not other processes. If you'd like to run multiple +instances of Certbot simultaneously you should specify different directories +as the ``--work-dir``, ``--logs-dir``, and ``--config-dir`` for each instance +of Certbot that you would like to run. .. _config-file: @@ -754,6 +806,18 @@ 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 @@ -770,9 +834,8 @@ Getting help If you're having problems, we recommend posting on the Let's Encrypt `Community Forum `_. -You can also chat with us on IRC: `(#certbot @ -OFTC) `_ or -`(#letsencrypt @ freenode) `_. +You can also chat with us on IRC: `(#letsencrypt @ +freenode) `_ If you find a bug in the software, please do report it in our `issue tracker `_. Remember to diff --git a/letsencrypt-auto b/letsencrypt-auto index c88c96a5b..599538891 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.14.2" +LE_AUTO_VERSION="0.16.0" BASENAME=$(basename $0) USAGE="Usage: $BASENAME [OPTIONS] A self-updating wrapper script for the Certbot ACME client. When run, updates @@ -694,7 +694,7 @@ if [ "$1" = "--le-auto-phase2" ]; then # Hashin example: # pip install hashin -# hashin -r letsencrypt-auto-requirements.txt cryptography==1.5.2 +# hashin -r dependency-requirements.txt cryptography==1.5.2 # sets the new certbot-auto pinned version of cryptography to 1.5.2 argparse==1.4.0 \ @@ -707,59 +707,78 @@ pycparser==2.14 \ --hash=sha256:7959b4a74abdc27b312fed1c21e6caf9309ce0b29ea86b591fd2e99ecdf27f73 \ --no-binary pycparser -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 +asn1crypto==0.22.0 \ + --hash=sha256:d232509fefcfcdb9a331f37e9c9dc20441019ad927c7d2176cf18ed5da0ba097 \ + --hash=sha256:cbbadd640d3165ab24b06ef25d1dca09a3441611ac15f6a6b452474fdf0aed1a +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.5.3 \ - --hash=sha256:e514d92086246b53ae9b048df652cf3036b462e50a6ce9fac6b6253502679991 \ - --hash=sha256:10ee414f4b5af403a0d8f20dfa80f7dad1fc7ae5452ec5af03712d5b6e78c664 \ - --hash=sha256:7234456d1f4345a144ed07af2416c7c0659d4bb599dd1a963103dc8c183b370e \ - --hash=sha256:d3b9587406f94642bd70b3d666b813f446e95f84220c9e416ad94cbfb6be2eaa \ - --hash=sha256:b15fc6b59f1474eef62207c85888afada8acc47fae8198ba2b0197d54538961a \ - --hash=sha256:3b62d65d342704fc07ed171598db2a2775bdf587b1b6abd2cba2261bfe3ccde3 \ - --hash=sha256:059343022ec904c867a13bc55d2573e36c8cfb2c250e30d8a2e9825f253b07ba \ - --hash=sha256:c7897cf13bc8b4ee0215d83cbd51766d87c06b277fcca1f9108595508e5bcfb4 \ - --hash=sha256:9b69e983e5bf83039ddd52e52a28c7faedb2b22bdfb5876377b95aac7d3be63e \ - --hash=sha256:61e40905c426d02b3fae38088dc66ce4ef84830f7eb223dec6b3ac3ccdc676fb \ - --hash=sha256:00783a32bcd91a12177230d35bfcf70a2333ade4a6b607fac94a633a7971c671 \ - --hash=sha256:d11973f49b648cde1ea1a30e496d7557dbfeccd08b3cd9ba58d286a9c274ff8e \ - --hash=sha256:f24bedf28b81932ba6063aec9a826669f5237ea3b755efe04d98b072faa053a5 \ - --hash=sha256:3ab5725367239e3deb9b92e917aa965af3fef008f25b96a3000821869e208181 \ - --hash=sha256:8a53209de822e22b5f73bf4b99e68ac4ccc91051fd6751c8252982983e86a77d \ - --hash=sha256:5a07439d4b1e4197ac202b7eea45e26a6fd65757652dc50f1a63367f711df933 \ - --hash=sha256:26b1c4b40aec7b0074bceabe6e06565aa28176eca7323a31df66ebf89fe916d3 \ - --hash=sha256:eaa4a7b5a6682adcf8d6ebb2a08a008802657643655bb527c95c8a3860253d8e \ - --hash=sha256:8156927dcf8da274ff205ad0612f75c380df45385bacf98531a5b3348c88d135 \ - --hash=sha256:61ec0d792749d0e91e84b1d58b6dfd204806b10b5811f846c2ceca0de028c53a \ - --hash=sha256:26330c88041569ca621cc42274d0ea2667a48b6deab41467272c3aba0b6e8f07 \ - --hash=sha256:cf82ddac919b587f5e44247579b433224cc2e03332d2ea4d89aa70d7e6b64ae5 +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 enum34==1.1.2 \ --hash=sha256:2475d7fcddf5951e92ff546972758802de5260bf409319a9f1934e6bbc8b1dc7 \ --hash=sha256:35907defb0f992b75ab7788f65fedc1cf20ffa22688e0e6f6f12afc06b3ea501 -funcsigs==0.4 \ - --hash=sha256:ff5ad9e2f8d9e5d1e8bbfbcf47722ab527cf0d51caeeed9da6d0f40799383fde \ - --hash=sha256:d83ce6df0b0ea6618700fe1db353526391a8a3ada1b7aba52fed7a61da772033 -idna==2.0 \ - --hash=sha256:9b2fc50bd3c4ba306b9651b69411ef22026d4d8335b93afc2214cef1246ce707 \ - --hash=sha256:16199aad938b290f5be1057c0e1efc6546229391c23cea61ca940c115f7d3d3b +funcsigs==1.0.2 \ + --hash=sha256:330cc27ccbf7f1e992e69fef78261dc7c6569012cf397db8d3de0234e6c937ca \ + --hash=sha256:a7bb0f2cf3a3fd1ab2732cb49eba4252c2af4240442415b4abce3b87022a8f50 +idna==2.5 \ + --hash=sha256:cc19709fd6d0cbfed39ea875d29ba6d4e22c0cebc510a76d6302a28385e8bb70 \ + --hash=sha256:3cb5ce08046c4e3a560fc02f138d0ac63e00f8ce5901a56b32ec8b7994082aab ipaddress==1.0.16 \ --hash=sha256:935712800ce4760701d89ad677666cd52691fd2f6f0b340c8b4239a3c17988a5 \ --hash=sha256:5a3182b322a706525c46282ca6f064d27a02cffbd449f9f47416f1dc96aa71b0 @@ -768,24 +787,15 @@ linecache2==1.0.0 \ --hash=sha256:4b26ff4e7110db76eeb6f5a7b64a82623839d595c2038eeda662f2a2db78e97c ordereddict==1.1 \ --hash=sha256:1c35b4ac206cef2d24816c89f89cf289dd3d38cf7c449bb3fab7bf6d43f01b1f +packaging==16.8 \ + --hash=sha256:99276dc6e3a7851f32027a68f1095cd3f77c148091b092ea867a351811cfe388 \ + --hash=sha256:5d50835fdf0a7edf0b55e311b7c887786504efea1177abd7e69329a8e5ea619e parsedatetime==2.1 \ --hash=sha256:ce9d422165cf6e963905cd5f74f274ebf7cc98c941916169178ef93f0e557838 \ --hash=sha256:17c578775520c99131634e09cfca5a05ea9e1bd2a05cd06967ebece10df7af2d pbr==1.8.1 \ --hash=sha256:46c8db75ae75a056bd1cc07fa21734fe2e603d11a07833ecc1eeb74c35c72e0c \ --hash=sha256:e2127626a91e6c885db89668976db31020f0af2da728924b56480fc7ccf09649 -pyasn1==0.1.9 \ - --hash=sha256:61f9d99e3cef65feb1bfe3a2eef7a93eb93819d345bf54bcd42f4e63d5204dae \ - --hash=sha256:1802a6dd32045e472a419db1441aecab469d33e0d2749e192abdec52101724af \ - --hash=sha256:35025cd9422c96504912f04e2f15fe79390a8597b430c2ca5d0534cf9309ffa0 \ - --hash=sha256:2f96ed5a0c329ca16230b326ca12b7461ec8f65e0be3e4f997516f36bf82a345 \ - --hash=sha256:28fee44217991cfad9e6a0b9f7e3f26041e21ebc96629e94e585ccd05d49fa65 \ - --hash=sha256:326e7a854a17fab07691204747695f8f692d674588a355c441fb14f660bf4e68 \ - --hash=sha256:cda5a90485709ca6795c86056c3e5fe7266028b05e53f1d527fdf93a6365a6b8 \ - --hash=sha256:0cb2a14742b543fdd68f931a14ce3829186ed2b1b2267a06787388c96b2dd9be \ - --hash=sha256:5191ff6b9126d2c039dd87f8ff025bed274baf07fa78afa46f556b1ad7265d6e \ - --hash=sha256:8323e03637b2d072cc7041300bac6ec448c3c28950ab40376036788e9a1af629 \ - --hash=sha256:853cacd96d1f701ddd67aa03ecc05f51890135b7262e922710112f12a2ed2a7f pyOpenSSL==16.2.0 \ --hash=sha256:26ca380ddf272f7556e48064bbcd5bd71f83dfc144f3583501c7ddbd9434ee17 \ --hash=sha256:7779a3bbb74e79db234af6a08775568c6769b5821faecf6e2f4143edb227516e @@ -851,27 +861,33 @@ zope.interface==4.1.3 \ --hash=sha256:928138365245a0e8869a5999fbcc2a45475a0a6ed52a494d60dbdc540335fedd \ --hash=sha256:0d841ba1bb840eea0e6489dc5ecafa6125554971f53b5acb87764441e61bceba \ --hash=sha256:b09c8c1d47b3531c400e0195697f1414a63221de6ef478598a4f1460f7d9a392 -mock==1.0.1 \ - --hash=sha256:b839dd2d9c117c701430c149956918a423a9863b48b09c90e30a6013e7d2f44f \ - --hash=sha256:8f83080daa249d036cbccfb8ae5cc6ff007b88d6d937521371afabe7b19badbc +mock==2.0.0 \ + --hash=sha256:5ce3c71c5545b472da17b72268978914d0252980348636840bd34a00b5cc96c1 \ + --hash=sha256:b158b6df76edd239b8208d481dc46b6afd45a846b7812ff0ce58971cf5bc8bba + +# Contains the requirements for the letsencrypt package. +# +# Since the letsencrypt package depends on certbot and using pip with hashes +# requires that all installed packages have hashes listed, this allows +# dependency-requirements.txt to be used without requiring a hash for a +# (potentially unreleased) Certbot package. + letsencrypt==0.7.0 \ --hash=sha256:105a5fb107e45bcd0722eb89696986dcf5f08a86a321d6aef25a0c7c63375ade \ --hash=sha256:c36e532c486a7e92155ee09da54b436a3c420813ec1c590b98f635d924720de9 -# THE LINES BELOW ARE EDITED BY THE RELEASE SCRIPT; ADD ALL DEPENDENCIES ABOVE. - -acme==0.14.2 \ - --hash=sha256:b3068d360beccd3b23a81d7cd2522437d847328811b573a5fe14eb04147667cf \ - --hash=sha256:166b7f4858f5b144b03236b995b787a9da1e410121fb7dcac9c7d3b594bc6fcd -certbot==0.14.2 \ - --hash=sha256:525e15e43c833db9a9934308d69dcdd220fa799488cd84543748671c68aba73d \ - --hash=sha256:5bc8547dcfc0fc587e15253e264f79d8397e48bfbc8697d5aca87eae978769ac -certbot-apache==0.14.2 \ - --hash=sha256:15647d424a5a7e4c44c684324ac07a457a2e0d61fce1acaa421c0b641941a350 \ - --hash=sha256:e5220d3e6ee5114b41b398110dfbd8f13bd1e8c7902758634449e0b4ae515b76 -certbot-nginx==0.14.2 \ - --hash=sha256:231377fbdfb6303adddc73fe3f856b9fb6d0175db825650e39fe3dfd6a58f8ef \ - --hash=sha256:529a18280acf41f5f7e0fe7d82c0a5d4d197d14f82742eaec54bb1d3f69c325a +certbot==0.16.0 \ + --hash=sha256:e1cc2479f4f149a7128b67192c3f5f6c111b6b9ddcac421ebee8ac5030d5b09b \ + --hash=sha256:795801fd6b06b32060e364ac045312e6b26c6272d5ca32878277e5a2afdee186 +acme==0.16.0 \ + --hash=sha256:31592a744f7a6e7cbb4c5daf2deebc9af6b03997d6f80e5becc203ab8694edcf \ + --hash=sha256:538b69134cc50bdcdcc081b844c85158509022c61d7abc1f44216beeb95de9cd +certbot-apache==0.16.0 \ + --hash=sha256:337f84f391c7d7c31d84376e7a8c882ac119112a4aabceadd1a7de6a2f01ca06 \ + --hash=sha256:f1f37b56e1ceb0a28ccf51d54ca34444e6a708448845ef25372e223b9a82c4d4 +certbot-nginx==0.16.0 \ + --hash=sha256:1d57428202f8e7abdd99c3ddbcf374aad5f69c4262fe8dae53d2016e4ae04ded \ + --hash=sha256:77711655514d707ddf728195f00b2f6cdd1ad9db52440276b4a67664479c6954 UNLIKELY_EOF # ------------------------------------------------------------------------- diff --git a/letsencrypt-auto-source/certbot-auto.asc b/letsencrypt-auto-source/certbot-auto.asc index 2650e5922..aa713c8c0 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 -iQEcBAABCAAGBQJZJ0tAAAoJEE0XyZXNl3Xyta0H/3+UZ1xeCc7CjZBMEMjb6IPm -h0KhptkLfwRR0/vGhTeIaOi8rzZYPuzZVwRvTuJ30oORI/zP+siGTOVW4Rt/3KI0 -IZidCJkdl3259jtJpSR9dWOXVp8bklZin8k6daQjbizq8Hl6z0aFLbHlqeSAZhUX -ush94CQwB380OUBut+g3CYx4BxD0dgTODPIaVYzeG8lOX5SXAaBbH79BOAtCr9Hy -sRfYjcBo4aL3rPCayPn+ETvQsYYo/Z7zqHjfShiKzZXNtW+RBGXAf8CGoEk7LKM4 -jts6PxOpg2BFpArDKHn6JIWsHOphBAQ/qIIgvD1mKZj6P4hGBJv4+aZ3Q8uhHeg= -=56Pv +iQEcBAABCAAGBQJZXV5PAAoJEE0XyZXNl3Xy3IgH/1OiY21IcAGx13jW32KNIsf9 +/vqjR/fyPbraSzYDldCVIQxsaVA6oh2kNZgiB11OpPT20/WsDq1+Ymj+dMjonJfm +w3wjx5AnfR/YThQwNXUJ83XnPCA78CtYiXus9gyqk+10WpNXUkbdGOwM4eYOtb3o +fNjJXkA00pATIYXks6qV0WJVEDNYuHDfkDfxukVgU7HzjfayLQjo4Zbs7Qnp7oH9 ++Lizc11nTliUUo3hHyLziJsCfC+Irso5Q/fHM9rn9DS360PcW0uNjWeLQk0U1w1l +tuuObOsDi/7Rejk4uDu6qDVemdEoG1btCrZgzOvzi3NstWigrL76cZuoz0gaGu4= +=mN1c -----END PGP SIGNATURE----- diff --git a/letsencrypt-auto-source/letsencrypt-auto b/letsencrypt-auto-source/letsencrypt-auto index aac67c071..023d5044e 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.15.0.dev0" +LE_AUTO_VERSION="0.17.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. @@ -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,66 @@ 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==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 enum34==1.1.2 \ --hash=sha256:2475d7fcddf5951e92ff546972758802de5260bf409319a9f1934e6bbc8b1dc7 \ --hash=sha256:35907defb0f992b75ab7788f65fedc1cf20ffa22688e0e6f6f12afc06b3ea501 @@ -864,18 +895,18 @@ letsencrypt==0.7.0 \ --hash=sha256:105a5fb107e45bcd0722eb89696986dcf5f08a86a321d6aef25a0c7c63375ade \ --hash=sha256:c36e532c486a7e92155ee09da54b436a3c420813ec1c590b98f635d924720de9 -acme==0.14.1 \ - --hash=sha256:f535d6459dcafa436749a8d2fdfafed21b792efa05b8bd3263fcd739c2e1497c \ - --hash=sha256:0e6d9d1bbb71d80c61c8d10ab9a40bcf38e25f0fa016b9769e96ebf5a79b552b -certbot==0.14.1 \ - --hash=sha256:f950a058d4f657160de4ad163d9f781fe7adeec0c0a44556841adb03ad135d13 \ - --hash=sha256:519b28124869d97116cb1f2f04ccc2937c0b2fd32fce43576eb80c0e4ff1ab65 -certbot-apache==0.14.1 \ - --hash=sha256:1dda9b4dcf66f6dfba37c787d849e69ad25a344572f74a76fc4447bb1a5417b2 \ - --hash=sha256:da84996e345fc5789da3575225536b27fa3b35f89b2db2d8f494a34bced14f9b -certbot-nginx==0.14.1 \ - --hash=sha256:bd3d4a1dcd6fa9e8ead19a9da88693f08b63464c86be2442e42cd60565c3f05f \ - --hash=sha256:f0c19f667072e4cfa6b92abf8312b6bee3ed1d2432676b211593034e7d1abb7e +certbot==0.16.0 \ + --hash=sha256:e1cc2479f4f149a7128b67192c3f5f6c111b6b9ddcac421ebee8ac5030d5b09b \ + --hash=sha256:795801fd6b06b32060e364ac045312e6b26c6272d5ca32878277e5a2afdee186 +acme==0.16.0 \ + --hash=sha256:31592a744f7a6e7cbb4c5daf2deebc9af6b03997d6f80e5becc203ab8694edcf \ + --hash=sha256:538b69134cc50bdcdcc081b844c85158509022c61d7abc1f44216beeb95de9cd +certbot-apache==0.16.0 \ + --hash=sha256:337f84f391c7d7c31d84376e7a8c882ac119112a4aabceadd1a7de6a2f01ca06 \ + --hash=sha256:f1f37b56e1ceb0a28ccf51d54ca34444e6a708448845ef25372e223b9a82c4d4 +certbot-nginx==0.16.0 \ + --hash=sha256:1d57428202f8e7abdd99c3ddbcf374aad5f69c4262fe8dae53d2016e4ae04ded \ + --hash=sha256:77711655514d707ddf728195f00b2f6cdd1ad9db52440276b4a67664479c6954 UNLIKELY_EOF # ------------------------------------------------------------------------- @@ -1033,9 +1064,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 index 032f73ca0..72ade22dc 100644 Binary files a/letsencrypt-auto-source/letsencrypt-auto.sig and b/letsencrypt-auto-source/letsencrypt-auto.sig differ 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/certbot-requirements.txt b/letsencrypt-auto-source/pieces/certbot-requirements.txt index 90b47a3cb..3c91c6e76 100644 --- a/letsencrypt-auto-source/pieces/certbot-requirements.txt +++ b/letsencrypt-auto-source/pieces/certbot-requirements.txt @@ -1,12 +1,12 @@ -acme==0.14.1 \ - --hash=sha256:f535d6459dcafa436749a8d2fdfafed21b792efa05b8bd3263fcd739c2e1497c \ - --hash=sha256:0e6d9d1bbb71d80c61c8d10ab9a40bcf38e25f0fa016b9769e96ebf5a79b552b -certbot==0.14.1 \ - --hash=sha256:f950a058d4f657160de4ad163d9f781fe7adeec0c0a44556841adb03ad135d13 \ - --hash=sha256:519b28124869d97116cb1f2f04ccc2937c0b2fd32fce43576eb80c0e4ff1ab65 -certbot-apache==0.14.1 \ - --hash=sha256:1dda9b4dcf66f6dfba37c787d849e69ad25a344572f74a76fc4447bb1a5417b2 \ - --hash=sha256:da84996e345fc5789da3575225536b27fa3b35f89b2db2d8f494a34bced14f9b -certbot-nginx==0.14.1 \ - --hash=sha256:bd3d4a1dcd6fa9e8ead19a9da88693f08b63464c86be2442e42cd60565c3f05f \ - --hash=sha256:f0c19f667072e4cfa6b92abf8312b6bee3ed1d2432676b211593034e7d1abb7e +certbot==0.16.0 \ + --hash=sha256:e1cc2479f4f149a7128b67192c3f5f6c111b6b9ddcac421ebee8ac5030d5b09b \ + --hash=sha256:795801fd6b06b32060e364ac045312e6b26c6272d5ca32878277e5a2afdee186 +acme==0.16.0 \ + --hash=sha256:31592a744f7a6e7cbb4c5daf2deebc9af6b03997d6f80e5becc203ab8694edcf \ + --hash=sha256:538b69134cc50bdcdcc081b844c85158509022c61d7abc1f44216beeb95de9cd +certbot-apache==0.16.0 \ + --hash=sha256:337f84f391c7d7c31d84376e7a8c882ac119112a4aabceadd1a7de6a2f01ca06 \ + --hash=sha256:f1f37b56e1ceb0a28ccf51d54ca34444e6a708448845ef25372e223b9a82c4d4 +certbot-nginx==0.16.0 \ + --hash=sha256:1d57428202f8e7abdd99c3ddbcf374aad5f69c4262fe8dae53d2016e4ae04ded \ + --hash=sha256:77711655514d707ddf728195f00b2f6cdd1ad9db52440276b4a67664479c6954 diff --git a/letsencrypt-auto-source/pieces/dependency-requirements.txt b/letsencrypt-auto-source/pieces/dependency-requirements.txt index 807c1c812..a8007ba3e 100644 --- a/letsencrypt-auto-source/pieces/dependency-requirements.txt +++ b/letsencrypt-auto-source/pieces/dependency-requirements.txt @@ -21,54 +21,66 @@ 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==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 enum34==1.1.2 \ --hash=sha256:2475d7fcddf5951e92ff546972758802de5260bf409319a9f1934e6bbc8b1dc7 \ --hash=sha256:35907defb0f992b75ab7788f65fedc1cf20ffa22688e0e6f6f12afc06b3ea501 diff --git a/tests/boulder-integration.sh b/tests/boulder-integration.sh index 5c00be054..181dcc5fb 100755 --- a/tests/boulder-integration.sh +++ b/tests/boulder-integration.sh @@ -13,12 +13,6 @@ set -eux . ./tests/integration/_common.sh export PATH="$PATH:/usr/sbin" # /usr/sbin/nginx -if [ `uname` = "Darwin" ];then - readlink="greadlink" -else - readlink="readlink" -fi - cleanup_and_exit() { EXIT_STATUS=$? if SERVER_STILL_RUNNING=`ps -p $python_server_pid -o pid=` @@ -58,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 @@ -80,6 +78,52 @@ 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 + # test for regressions of #4719 get_num_tmp_files() { ls -1 /tmp | wc -l @@ -94,32 +138,53 @@ if [ $(get_num_tmp_files) -ne $num_tmp_files ]; then exit 1 fi +common register +common register --update-registration --email example@example.org + +common plugins --init --prepare | grep webroot + # We start a server listening on the port for the # unrequested challenge to prevent regressions in #3601. 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"' + --post-hook 'echo wtf2.post >> "$HOOK_TEST"' \ + --renew-hook 'echo deploy >> "$HOOK_TEST"' +CheckRenewHook $certname -common certonly -a manual -d dns.le.wtf --preferred-challenges dns,tls-sni \ - --manual-auth-hook ./tests/manual-dns-auth.sh +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 @@ -217,9 +282,33 @@ common revoke --cert-path "$root/conf/live/le2.wtf/cert.pem" \ common unregister +out=$(common certificates) +subdomains="le le2 dns.le newname.le must-staple.le" +for subdomain in $subdomains; do + domain="$subdomain.wtf" + if ! echo $out | grep "$domain"; then + echo "$domain not in certificates output!" + exit 1; + fi +done + +cert_name="must-staple.le.wtf" +common delete --cert-name $cert_name +archive="$root/conf/archive/$cert_name" +conf="$root/conf/renewal/$cert_name.conf" +live="$root/conf/live/$cert_name" +for path in $archive $conf $live; do + if [ -e $path ]; then + echo "Lineage not properly deleted!" + exit 1 + fi +done + # Most CI systems set this variable to true. # If the tests are running as part of CI, Nginx should be available. if ${CI:-false} || type nginx; then . ./certbot-nginx/tests/boulder-integration.sh fi + +coverage report --fail-under 64 -m diff --git a/tests/integration/_common.sh b/tests/integration/_common.sh index 8d4baff95..d151bdc3f 100755 --- a/tests/integration/_common.sh +++ b/tests/integration/_common.sh @@ -2,11 +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 -export root store_flags tls_sni_01_port http_01_port +sources="acme/,$(ls -dm certbot*/ | tr -d ' \n')" +export root config_dir store_flags tls_sni_01_port http_01_port sources certbot_test () { certbot_test_no_force_renew \ @@ -15,18 +17,25 @@ certbot_test () { } certbot_test_no_force_renew () { - certbot \ - --server "${SERVER:-http://localhost:4000/directory}" \ - --no-verify-ssl \ - --tls-sni-01-port $tls_sni_01_port \ - --http-01-port $http_01_port \ - --manual-public-ip-logging-ok \ - $store_flags \ - --non-interactive \ - --no-redirect \ - --agree-tos \ - --register-unsafely-without-email \ - --debug \ - -vv \ - "$@" + omit_patterns="*/*.egg-info/*,*/dns_common*,*/setup.py,*/test_*,*/tests/*" + omit_patterns="$omit_patterns,*_test.py,*_test_*," + omit_patterns="$omit_patterns,certbot-compatibility-test/*,certbot-dns*/" + coverage run \ + --append \ + --source $sources \ + --omit $omit_patterns \ + $(command -v certbot) \ + --server "${SERVER:-http://localhost:4000/directory}" \ + --no-verify-ssl \ + --tls-sni-01-port $tls_sni_01_port \ + --http-01-port $http_01_port \ + --manual-public-ip-logging-ok \ + $store_flags \ + --non-interactive \ + --no-redirect \ + --agree-tos \ + --register-unsafely-without-email \ + --debug \ + -vv \ + "$@" } diff --git a/tests/letstest/scripts/test_apache2.sh b/tests/letstest/scripts/test_apache2.sh index 7fa860302..6b5d63c80 100755 --- a/tests/letstest/scripts/test_apache2.sh +++ b/tests/letstest/scripts/test_apache2.sh @@ -45,7 +45,7 @@ if [ $? -ne 0 ] ; then exit 1 fi -tools/venv.sh +tools/_venv_common.sh -e acme[dev] -e .[dev,docs] -e certbot-apache sudo venv/bin/certbot -v --debug --text --agree-dev-preview --agree-tos \ --renew-by-default --redirect --register-unsafely-without-email \ --domain $PUBLIC_HOSTNAME --server $BOULDER_URL diff --git a/tools/install_and_test.sh b/tools/install_and_test.sh new file mode 100755 index 000000000..0de2ea3f8 --- /dev/null +++ b/tools/install_and_test.sh @@ -0,0 +1,21 @@ +#!/bin/sh -e +# pip installs the requested packages in editable mode and runs unit tests on +# them. Each package is installed and tested in the order they are provided +# before the script moves on to the next package. If CERTBOT_NO_PIN is set not +# set to 1, packages are installed using certbot-auto's requirements file as +# constraints. + +if [ "$CERTBOT_NO_PIN" = 1 ]; then + pip_install="pip install -e" +else + pip_install="$(dirname $0)/pip_install_editable.sh" +fi + +for requirement in "$@" ; do + $pip_install $requirement + pkg=$(echo $requirement | cut -f1 -d\[) # remove any extras such as [dev] + if [ $pkg = "." ]; then + pkg="certbot" + fi + nosetests -v $pkg --processes=-1 --process-timeout=100 +done diff --git a/tools/pip_install.sh b/tools/pip_install.sh index 8a58f9e48..438e567e4 100755 --- a/tools/pip_install.sh +++ b/tools/pip_install.sh @@ -2,7 +2,8 @@ # pip installs packages using Certbot's requirements file as constraints # get the root of the Certbot repo -repo_root=$(git rev-parse --show-toplevel) +my_path=$("$(dirname $0)/readlink.py" $0) +repo_root=$(dirname $(dirname $my_path)) requirements="$repo_root/letsencrypt-auto-source/pieces/dependency-requirements.txt" constraints=$(mktemp) trap "rm -f $constraints" EXIT diff --git a/tools/pip_install_editable.sh b/tools/pip_install_editable.sh new file mode 100755 index 000000000..6130bf6e7 --- /dev/null +++ b/tools/pip_install_editable.sh @@ -0,0 +1,10 @@ +#!/bin/sh -e +# pip installs packages in editable mode using certbot-auto's requirements file +# as constraints + +args="" +for requirement in "$@" ; do + args="$args -e $requirement" +done + +"$(dirname $0)/pip_install.sh" $args diff --git a/tools/readlink.py b/tools/readlink.py new file mode 100755 index 000000000..02c74c48d --- /dev/null +++ b/tools/readlink.py @@ -0,0 +1,13 @@ +#!/usr/bin/env python +"""Canonicalizes a path and follows any symlinks. + +This is the equivalent of `readlink -f` on many Linux systems. This is +useful as there are often differences in readlink on different +platforms. + +""" +from __future__ import print_function +import os +import sys + +print(os.path.realpath(sys.argv[1])) diff --git a/tools/release.sh b/tools/release.sh index 3807cea32..2a8e00aa1 100755 --- a/tools/release.sh +++ b/tools/release.sh @@ -46,7 +46,7 @@ PORT=${PORT:-1234} # subpackages to be released (the way developers think about them) SUBPKGS_IN_AUTO_NO_CERTBOT="acme certbot-apache certbot-nginx" -SUBPKGS_NOT_IN_AUTO="certbot-dns-cloudxns certbot-dns-dnsimple certbot-dns-nsone" +SUBPKGS_NOT_IN_AUTO="certbot-dns-cloudflare certbot-dns-cloudxns certbot-dns-digitalocean certbot-dns-dnsimple certbot-dns-dnsmadeeasy certbot-dns-google certbot-dns-luadns certbot-dns-nsone certbot-dns-rfc2136 certbot-dns-route53" # subpackages to be released (the way the script thinks about them) SUBPKGS_IN_AUTO="certbot $SUBPKGS_IN_AUTO_NO_CERTBOT" diff --git a/tools/venv.sh b/tools/venv.sh index 75ef017be..1533f0e1f 100755 --- a/tools/venv.sh +++ b/tools/venv.sh @@ -18,9 +18,12 @@ fi -e certbot-dns-cloudxns \ -e certbot-dns-digitalocean \ -e certbot-dns-dnsimple \ + -e certbot-dns-dnsmadeeasy \ -e certbot-dns-google \ + -e certbot-dns-luadns \ -e certbot-dns-nsone \ + -e certbot-dns-rfc2136 \ + -e certbot-dns-route53 \ -e certbot-nginx \ - -e certbot-route53 \ -e letshelp-certbot \ -e certbot-compatibility-test diff --git a/tools/venv3.sh b/tools/venv3.sh index 6f5d96262..da56c2249 100755 --- a/tools/venv3.sh +++ b/tools/venv3.sh @@ -17,9 +17,11 @@ fi -e certbot-dns-cloudxns \ -e certbot-dns-digitalocean \ -e certbot-dns-dnsimple \ + -e certbot-dns-dnsmadeeasy \ -e certbot-dns-google \ + -e certbot-dns-luadns \ -e certbot-dns-nsone \ + -e certbot-dns-route53 \ -e certbot-nginx \ - -e certbot-route53 \ -e letshelp-certbot \ -e certbot-compatibility-test diff --git a/tox.cover.sh b/tox.cover.sh index eabe6ba7e..fc0c9f476 100755 --- a/tox.cover.sh +++ b/tox.cover.sh @@ -9,7 +9,7 @@ # -e makes sure we fail fast and don't submit coveralls submit if [ "xxx$1" = "xxx" ]; then - pkgs="certbot acme certbot_apache certbot_dns_cloudflare certbot_dns_cloudxns certbot_dns_digitalocean certbot_dns_dnsimple certbot_dns_google certbot_dns_nsone certbot_nginx certbot_route53 letshelp_certbot" + pkgs="certbot acme certbot_apache certbot_dns_cloudflare certbot_dns_cloudxns certbot_dns_digitalocean certbot_dns_dnsimple certbot_dns_dnsmadeeasy certbot_dns_google certbot_dns_luadns certbot_dns_nsone certbot_dns_rfc2136 certbot_dns_route53 certbot_nginx letshelp_certbot" else pkgs="$@" fi @@ -29,14 +29,20 @@ cover () { min=98 elif [ "$1" = "certbot_dns_dnsimple" ]; then min=98 + elif [ "$1" = "certbot_dns_dnsmadeeasy" ]; then + min=99 elif [ "$1" = "certbot_dns_google" ]; then min=99 + elif [ "$1" = "certbot_dns_luadns" ]; then + min=98 elif [ "$1" = "certbot_dns_nsone" ]; then min=99 + elif [ "$1" = "certbot_dns_rfc2136" ]; then + min=99 + elif [ "$1" = "certbot_dns_route53" ]; then + min=99 elif [ "$1" = "certbot_nginx" ]; then min=97 - elif [ "$1" = "certbot_route53" ]; then - min=99 elif [ "$1" = "letshelp_certbot" ]; then min=100 else diff --git a/tox.ini b/tox.ini index 414dac790..299b7f20f 100644 --- a/tox.ini +++ b/tox.ini @@ -10,102 +10,108 @@ envlist = modification,py{26,33,34,35,36},cover,lint # loops, especially on Travis [base] -# wraps pip install to use pinned versions of dependencies -pip_install = {toxinidir}/tools/pip_install.sh -# packages installed separately to ensure that downstream deps problems -# are detected, c.f. #1002 -core_commands = - {[base]pip_install} -e acme[dev] - nosetests -v acme --processes=-1 - {[base]pip_install} -e .[dev] - nosetests -v certbot --processes=-1 --process-timeout=100 -core_install_args = -e acme[dev] -e .[dev] -core_paths = acme/acme certbot - -plugin_commands = - {[base]pip_install} -e certbot-apache - nosetests -v certbot_apache --processes=-1 --process-timeout=80 - {[base]pip_install} -e certbot-nginx - nosetests -v certbot_nginx --processes=-1 -plugin_install_args = -e certbot-apache -e certbot-nginx -plugin_paths = certbot-apache/certbot_apache certbot-nginx/certbot_nginx - -dns_plugin_commands = - pip install -e certbot-dns-cloudflare - nosetests -v certbot_dns_cloudflare --processes=-1 - pip install -e certbot-dns-digitalocean - nosetests -v certbot_dns_digitalocean --processes=-1 - pip install -e certbot-dns-google - nosetests -v certbot_dns_google --processes=-1 - pip install -e certbot-route53 - nosetests -v certbot_route53 --processes=-1 --process-timeout=25 -dns_plugin_install_args = -e certbot-dns-cloudflare -e certbot-dns-digitalocean -e certbot-dns-google -e certbot-route53 -dns_plugin_paths = certbot-dns-cloudflare/certbot_dns_cloudflare certbot-dns-digitalocean/certbot_dns_digitalocean certbot-dns-google/certbot_dns_google certbot-route53/certbot_route53 - -lexicon_dns_plugin_commands = - pip install -e certbot-dns-cloudxns - nosetests -v certbot_dns_cloudxns --processes=-1 - pip install -e certbot-dns-dnsimple - nosetests -v certbot_dns_dnsimple --processes=-1 - pip install -e certbot-dns-nsone - nosetests -v certbot_dns_nsone --processes=-1 -lexicon_dns_plugin_install_args = -e certbot-dns-cloudxns -e certbot-dns-dnsimple -e certbot-dns-nsone -lexicon_dns_plugin_paths = certbot-dns-cloudxns/certbot_dns_cloudxns certbot-dns-dnsimple/certbot_dns_dnsimple certbot-dns-nsone/certbot_dns_nsone - -compatibility_install_args = -e certbot-compatibility-test -compatibility_paths = certbot-compatibility-test/certbot_compatibility_test - -other_commands = - {[base]pip_install} -e letshelp-certbot - nosetests -v letshelp_certbot --processes=-1 - python tests/lock_test.py -other_install_args = -e letshelp-certbot -other_paths = letshelp-certbot/letshelp_certbot tests/lock_test.py - -[testenv] -commands = - {[base]core_commands} - {[base]plugin_commands} - {[base]dns_plugin_commands} - {[base]lexicon_dns_plugin_commands} - {[base]other_commands} - -setenv = - PYTHONPATH = {toxinidir} - PYTHONHASHSEED = 0 -# https://testrun.org/tox/latest/example/basic.html#special-handling-of-pythonhas - -# cffi<=1.7 is required for oldest tests due to -# https://bitbucket.org/cffi/cffi/commits/18cdf37d6b2691301a15b0e54f49757ebd4ed0f2?at=default -# requests<=2.11.1 required for oldest tests due to -# https://github.com/shazow/urllib3/pull/930 -deps = - py{26,27}-oldest: cffi<=1.7 - py{26,27}-oldest: cryptography==1.2 - py{26,27}-oldest: configargparse==0.10.0 - py{26,27}-oldest: PyOpenSSL==0.13 - py{26,27}-oldest: requests<=2.11.1 +# pip installs the requested packages in editable mode +pip_install = {toxinidir}/tools/pip_install_editable.sh +# pip installs the requested packages in editable mode and runs unit tests on +# them. Each package is installed and tested in the order they are provided +# before the script moves on to the next package. If CERTBOT_NO_PIN is set not +# set to 1, packages are installed using certbot-auto's requirements file as +# constraints. +install_and_test = {toxinidir}/tools/install_and_test.sh +py26_packages = + acme[dev] \ + .[dev] \ + certbot-apache \ + certbot-dns-cloudflare \ + certbot-dns-digitalocean \ + certbot-dns-google \ + certbot-dns-rfc2136 \ + certbot-dns-route53 \ + certbot-nginx \ + letshelp-certbot +non_py26_packages = + certbot-dns-cloudxns \ + certbot-dns-dnsimple \ + certbot-dns-dnsmadeeasy \ + certbot-dns-luadns \ + certbot-dns-nsone +all_packages = + {[base]py26_packages} {[base]non_py26_packages} +install_packages = + {toxinidir}/tools/pip_install_editable.sh {[base]all_packages} +source_paths = + acme/acme + certbot + certbot-apache/certbot_apache + certbot-compatibility-test/certbot_compatibility_test + certbot-dns-cloudflare/certbot_dns_cloudflare + certbot-dns-cloudxns/certbot_dns_cloudxns + certbot-dns-digitalocean/certbot_dns_digitalocean + certbot-dns-dnsimple/certbot_dns_dnsimple + certbot-dns-dnsmadeeasy/certbot_dns_dnsmadeeasy + certbot-dns-google/certbot_dns_google + certbot-dns-luadns/certbot_dns_luadns + certbot-dns-nsone/certbot_dns_nsone + certbot-dns-rfc2136/certbot_dns_rfc2136 + certbot-dns-route53/certbot_dns_route53 + certbot-nginx/certbot_nginx + letshelp-certbot/letshelp_certbot + tests/lock_test.py [testenv:py26] commands = - {[base]core_commands} - {[base]plugin_commands} - {[base]dns_plugin_commands} - {[base]other_commands} + {[base]install_and_test} {[base]py26_packages} + python tests/lock_test.py -[testenv:py26-oldest] +[testenv] commands = {[testenv:py26]commands} + {[base]install_and_test} {[base]non_py26_packages} +setenv = + PYTHONPATH = {toxinidir} + PYTHONHASHSEED = 0 + +[testenv:py27-oldest] +commands = + {[testenv]commands} +setenv = + {[testenv]setenv} + CERTBOT_NO_PIN=1 +deps = + PyOpenSSL==0.13 + cffi==1.5.2 + configargparse==0.10.0 + configargparse==0.10.0 + configobj==4.7.2 + cryptography==1.2.3 + enum34==0.9.23 + idna==2.0 + ipaddress==1.0.16 + mock==1.0.1 + ndg-httpsclient==0.3.2 + parsedatetime==1.4 + pyasn1==0.1.9 + pyparsing==1.5.6 + pyrfc3339==1.0 + python-augeas==0.4.1 + pytz==2012c + requests[security]==2.6.0 + setuptools==0.9.8 + six==1.9.0 + urllib3==1.10 + zope.component==4.0.2 + zope.event==4.0.1 + zope.interface==4.0.5 [testenv:py27_install] basepython = python2.7 commands = - {[base]pip_install} {[base]core_install_args} {[base]plugin_install_args} {[base]dns_plugin_install_args} {[base]lexicon_dns_plugin_install_args} {[base]other_install_args} + {[base]install_packages} [testenv:cover] basepython = python2.7 commands = - {[base]pip_install} {[base]core_install_args} {[base]plugin_install_args} {[base]dns_plugin_install_args} {[base]lexicon_dns_plugin_install_args} {[base]other_install_args} + {[base]install_packages} ./tox.cover.sh [testenv:lint] @@ -115,25 +121,25 @@ basepython = python2.7 # duplicate code checking; if one of the commands fails, others will # continue, but tox return code will reflect previous error commands = - {[base]pip_install} -q {[base]core_install_args} {[base]plugin_install_args} {[base]dns_plugin_install_args} {[base]lexicon_dns_plugin_install_args} {[base]compatibility_install_args} {[base]other_install_args} - pylint --reports=n --rcfile=.pylintrc {[base]core_paths} {[base]plugin_paths} {[base]dns_plugin_paths} {[base]lexicon_dns_plugin_paths} {[base]compatibility_paths} {[base]other_paths} + {[base]install_packages} + pylint --reports=n --rcfile=.pylintrc {[base]source_paths} [testenv:mypy] basepython = python3.4 commands = {[base]pip_install} mypy - {[base]pip_install} -q {[base]core_install_args} {[base]plugin_install_args} {[base]dns_plugin_install_args} {[base]lexicon_dns_plugin_install_args} {[base]compatibility_install_args} {[base]other_install_args} - mypy --py2 --ignore-missing-imports {[base]core_paths} {[base]plugin_paths} {[base]dns_plugin_paths} {[base]lexicon_dns_plugin_paths} {[base]compatibility_paths} {[base]other_paths} + {[base]install_packages} + mypy --py2 --ignore-missing-imports {[base]source_paths} [testenv:apacheconftest] #basepython = python2.7 commands = - {[base]pip_install} {[base]core_install_args} {[base]plugin_install_args} {[base]compatibility_install_args} {[base]other_install_args} + {[base]pip_install} acme . certbot-apache certbot-compatibility-test {toxinidir}/certbot-apache/certbot_apache/tests/apache-conf-files/apache-conf-test --debian-modules [testenv:nginxroundtrip] commands = - {[base]pip_install} {[base]core_install_args} -e certbot-nginx + {[base]pip_install} acme . certbot-apache certbot-nginx python certbot-compatibility-test/nginx/roundtrip.py certbot-compatibility-test/nginx/nginx-roundtrip-testdata # This is a duplication of the command line in testenv:le_auto to @@ -155,7 +161,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_*