diff --git a/.travis.yml b/.travis.yml
index 8101fb3a4..3a9a994a9 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -4,9 +4,6 @@ cache:
directories:
- $HOME/.cache/pip
-# This makes sure we get a host with docker-compose present.
-dist: trusty
-
before_install:
- 'dpkg -s libaugeas0'
@@ -20,43 +17,64 @@ env:
matrix:
include:
- python: "2.7"
- env: TOXENV=cover BOULDER_INTEGRATION=1
- sudo: true
- after_failure:
- - sudo cat /var/log/mysql/error.log
- - ps aux | grep mysql
+ env: TOXENV=cover
- python: "2.7"
env: TOXENV=lint
- python: "2.7"
env: TOXENV=py27-oldest BOULDER_INTEGRATION=1
- sudo: true
+ sudo: required
after_failure:
- sudo cat /var/log/mysql/error.log
- ps aux | grep mysql
+ services: docker
- python: "2.6"
env: TOXENV=py26 BOULDER_INTEGRATION=1
- sudo: true
+ sudo: required
after_failure:
- sudo cat /var/log/mysql/error.log
- ps aux | grep mysql
+ services: docker
- python: "2.6"
env: TOXENV=py26-oldest BOULDER_INTEGRATION=1
- sudo: true
+ 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
+ after_failure:
+ - sudo cat /var/log/mysql/error.log
+ - ps aux | grep mysql
+ services: docker
+ - sudo: required
+ env: TOXENV=apache_compat
+ services: docker
+ before_install:
+ addons:
- sudo: required
env: TOXENV=nginx_compat
services: docker
before_install:
addons:
- sudo: required
- env: TOXENV=le_auto
+ env: TOXENV=le_auto_precise
services: docker
before_install:
addons:
- sudo: required
- env: TOXENV=apache_compat
+ env: TOXENV=le_auto_trusty
+ services: docker
+ before_install:
+ addons:
+ - sudo: required
+ env: TOXENV=le_auto_wheezy
+ services: docker
+ before_install:
+ addons:
+ - sudo: required
+ env: TOXENV=le_auto_centos6
services: docker
before_install:
addons:
@@ -103,7 +121,6 @@ addons:
- python-dev
- python-virtualenv
- gcc
- - dialog
- libaugeas0
- libssl-dev
- libffi-dev
@@ -115,7 +132,6 @@ addons:
- apache2
- libapache2-mod-wsgi
- libapache2-mod-macro
- - sudo
install: "travis_retry pip install tox coveralls"
script: 'travis_retry tox && ([ "xxx$BOULDER_INTEGRATION" = "xxx" ] || ./tests/travis-integration.sh)'
diff --git a/Vagrantfile b/Vagrantfile
index e5975442f..23d3ddf13 100644
--- a/Vagrantfile
+++ b/Vagrantfile
@@ -29,6 +29,7 @@ Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
# VM needs more memory to run test suite, got "OSError: [Errno 12]
# Cannot allocate memory" when running
# letsencrypt.client.tests.display.util_test.NcursesDisplayTest
+ # We may no longer need this.
v.memory = 1024
# Handle cases when the host is behind a private network by making the
diff --git a/acme/acme/client.py b/acme/acme/client.py
index 6a648bb92..26109352b 100644
--- a/acme/acme/client.py
+++ b/acme/acme/client.py
@@ -1,4 +1,5 @@
"""ACME client API."""
+import base64
import collections
import datetime
from email.utils import parsedate_tz
@@ -26,7 +27,13 @@ logger = logging.getLogger(__name__)
# for SSL, which does allow these options to be configured.
# https://urllib3.readthedocs.org/en/latest/security.html#insecureplatformwarning
if sys.version_info < (2, 7, 9): # pragma: no cover
- requests.packages.urllib3.contrib.pyopenssl.inject_into_urllib3()
+ try:
+ requests.packages.urllib3.contrib.pyopenssl.inject_into_urllib3()
+ except AttributeError:
+ import urllib3.contrib.pyopenssl # pylint: disable=import-error
+ urllib3.contrib.pyopenssl.inject_into_urllib3()
+
+DER_CONTENT_TYPE = 'application/pkix-cert'
class Client(object): # pylint: disable=too-many-instance-attributes
@@ -45,7 +52,6 @@ class Client(object): # pylint: disable=too-many-instance-attributes
`verify_ssl`.
"""
- DER_CONTENT_TYPE = 'application/pkix-cert'
def __init__(self, directory, key, alg=jose.RS256, verify_ssl=True,
net=None):
@@ -304,7 +310,7 @@ class Client(object): # pylint: disable=too-many-instance-attributes
# TODO: assert len(authzrs) == number of SANs
req = messages.CertificateRequest(csr=csr)
- content_type = self.DER_CONTENT_TYPE # TODO: add 'cert_type 'argument
+ content_type = DER_CONTENT_TYPE # TODO: add 'cert_type 'argument
response = self.net.post(
authzrs[0].new_cert_uri, # TODO: acme-spec #90
req,
@@ -406,7 +412,7 @@ class Client(object): # pylint: disable=too-many-instance-attributes
:rtype: tuple
"""
- content_type = self.DER_CONTENT_TYPE # TODO: make it a param
+ content_type = DER_CONTENT_TYPE # TODO: make it a param
response = self.net.get(uri, headers={'Accept': content_type},
content_type=content_type)
return response, jose.ComparableX509(OpenSSL.crypto.load_certificate(
@@ -475,17 +481,21 @@ class Client(object): # pylint: disable=too-many-instance-attributes
"Recursion limit reached. Didn't get {0}".format(uri))
return chain
- def revoke(self, cert):
+ def revoke(self, cert, rsn):
"""Revoke certificate.
:param .ComparableX509 cert: `OpenSSL.crypto.X509` wrapped in
`.ComparableX509`
+ :param int rsn: Reason code for certificate revocation.
+
:raises .ClientError: If revocation is unsuccessful.
"""
response = self.net.post(self.directory[messages.Revocation],
- messages.Revocation(certificate=cert),
+ messages.Revocation(
+ certificate=cert,
+ reason=rsn),
content_type=None)
if response.status_code != http_client.OK:
raise errors.ClientError(
@@ -521,10 +531,11 @@ class ClientNetwork(object): # pylint: disable=too-many-instance-attributes
:rtype: `.JWS`
"""
- jobj = obj.json_dumps().encode()
- logger.debug('Serialized JSON: %s', jobj)
+ jobj = obj.json_dumps(indent=2).encode()
+ logger.debug('JWS payload:\n%s', jobj)
return jws.JWS.sign(
- payload=jobj, key=self.key, alg=self.alg, nonce=nonce).json_dumps()
+ payload=jobj, key=self.key, alg=self.alg,
+ nonce=nonce).json_dumps(indent=2)
@classmethod
def _check_response(cls, response, content_type=None):
@@ -545,9 +556,6 @@ class ClientNetwork(object): # pylint: disable=too-many-instance-attributes
:raises .ClientError: In case of other networking errors.
"""
- logger.debug('Received response %s (headers: %s): %r',
- response, response.headers, response.content)
-
response_ct = response.headers.get('Content-Type')
try:
# TODO: response.json() is called twice, once here, and
@@ -599,14 +607,26 @@ class ClientNetwork(object): # pylint: disable=too-many-instance-attributes
"""
- logging.debug('Sending %s request to %s. args: %r, kwargs: %r',
- method, url, args, kwargs)
+ if method == "POST":
+ logging.debug('Sending POST request to %s:\n%s',
+ url, kwargs['data'])
+ else:
+ logging.debug('Sending %s request to %s.', method, url)
kwargs['verify'] = self.verify_ssl
kwargs.setdefault('headers', {})
kwargs['headers'].setdefault('User-Agent', self.user_agent)
response = self.session.request(method, url, *args, **kwargs)
- logging.debug('Received %s. Headers: %s. Content: %r',
- response, response.headers, response.content)
+ # If content is DER, log the base64 of it instead of raw bytes, to keep
+ # binary data out of the logs.
+ if response.headers.get("Content-Type") == DER_CONTENT_TYPE:
+ debug_content = base64.b64encode(response.content)
+ else:
+ debug_content = response.content
+ logger.debug('Received response:\nHTTP %d\n%s\n\n%s',
+ response.status_code,
+ "\n".join(["{0}: {1}".format(k, v)
+ for k, v in response.headers.items()]),
+ debug_content)
return response
def head(self, *args, **kwargs):
@@ -631,7 +651,7 @@ class ClientNetwork(object): # pylint: disable=too-many-instance-attributes
decoded_nonce = jws.Header._fields['nonce'].decode(nonce)
except jose.DeserializationError as error:
raise errors.BadNonce(nonce, error)
- logger.debug('Storing nonce: %r', decoded_nonce)
+ logger.debug('Storing nonce: %s', nonce)
self._nonces.add(decoded_nonce)
else:
raise errors.MissingNonce(response)
diff --git a/acme/acme/client_test.py b/acme/acme/client_test.py
index 374f8954c..4822a1ae6 100644
--- a/acme/acme/client_test.py
+++ b/acme/acme/client_test.py
@@ -81,6 +81,9 @@ class ClientTest(unittest.TestCase):
uri='https://www.letsencrypt-demo.org/acme/cert/1',
cert_chain_uri='https://www.letsencrypt-demo.org/ca')
+ # Reason code for revocation
+ self.rsn = 1
+
def test_init_downloads_directory(self):
uri = 'http://www.letsencrypt-demo.org/directory'
from acme.client import Client
@@ -427,13 +430,22 @@ class ClientTest(unittest.TestCase):
self.assertRaises(errors.Error, self.client.fetch_chain, self.certr)
def test_revoke(self):
- self.client.revoke(self.certr.body)
+ self.client.revoke(self.certr.body, self.rsn)
self.net.post.assert_called_once_with(
self.directory[messages.Revocation], mock.ANY, content_type=None)
+ def test_revocation_payload(self):
+ obj = messages.Revocation(certificate=self.certr.body, reason=self.rsn)
+ self.assertTrue('reason' in obj.to_partial_json().keys())
+ self.assertEquals(self.rsn, obj.to_partial_json()['reason'])
+
def test_revoke_bad_status_raises_error(self):
self.response.status_code = http_client.METHOD_NOT_ALLOWED
- self.assertRaises(errors.ClientError, self.client.revoke, self.certr)
+ self.assertRaises(
+ errors.ClientError,
+ self.client.revoke,
+ self.certr,
+ self.rsn)
class ClientNetworkTest(unittest.TestCase):
@@ -534,6 +546,29 @@ class ClientNetworkTest(unittest.TestCase):
'HEAD', 'http://example.com/', 'foo',
headers=mock.ANY, verify=mock.ANY, bar='baz')
+ @mock.patch('acme.client.logger')
+ def test_send_request_get_der(self, mock_logger):
+ self.net.session = mock.MagicMock()
+ self.net.session.request.return_value = mock.MagicMock(
+ ok=True, status_code=http_client.OK,
+ headers={"Content-Type": "application/pkix-cert"},
+ content=b"hi")
+ # pylint: disable=protected-access
+ self.net._send_request('HEAD', 'http://example.com/', 'foo', bar='baz')
+ mock_logger.debug.assert_called_once_with(
+ 'Received response:\nHTTP %d\n%s\n\n%s', 200,
+ 'Content-Type: application/pkix-cert', b'aGk=')
+
+ def test_send_request_post(self):
+ self.net.session = mock.MagicMock()
+ self.net.session.request.return_value = self.response
+ # pylint: disable=protected-access
+ self.assertEqual(self.response, self.net._send_request(
+ 'POST', 'http://example.com/', 'foo', data='qux', bar='baz'))
+ self.net.session.request.assert_called_once_with(
+ 'POST', 'http://example.com/', 'foo',
+ headers=mock.ANY, verify=mock.ANY, data='qux', bar='baz')
+
def test_send_request_verify_ssl(self):
# pylint: disable=protected-access
for verify in True, False:
diff --git a/acme/acme/crypto_util.py b/acme/acme/crypto_util.py
index 2b2133475..266f2c0c7 100644
--- a/acme/acme/crypto_util.py
+++ b/acme/acme/crypto_util.py
@@ -63,6 +63,8 @@ class SSLSocket(object): # pylint: disable=too-few-public-methods
server_name)
return
new_context = OpenSSL.SSL.Context(self.method)
+ new_context.set_options(OpenSSL.SSL.OP_NO_SSLv2)
+ new_context.set_options(OpenSSL.SSL.OP_NO_SSLv3)
new_context.use_privatekey(key)
new_context.use_certificate(cert)
connection.set_context(new_context)
@@ -86,6 +88,8 @@ class SSLSocket(object): # pylint: disable=too-few-public-methods
sock, addr = self.sock.accept()
context = OpenSSL.SSL.Context(self.method)
+ context.set_options(OpenSSL.SSL.OP_NO_SSLv2)
+ context.set_options(OpenSSL.SSL.OP_NO_SSLv3)
context.set_tlsext_servername_callback(self._pick_certificate_cb)
ssl_sock = self.FakeConnection(OpenSSL.SSL.Connection(context, sock))
diff --git a/acme/acme/crypto_util_test.py b/acme/acme/crypto_util_test.py
index 75a908d4f..bd93ae0e1 100644
--- a/acme/acme/crypto_util_test.py
+++ b/acme/acme/crypto_util_test.py
@@ -18,9 +18,11 @@ from acme import test_util
class SSLSocketAndProbeSNITest(unittest.TestCase):
"""Tests for acme.crypto_util.SSLSocket/probe_sni."""
+ _multiprocess_can_split_ = True
+
def setUp(self):
- self.cert = test_util.load_comparable_cert('cert.pem')
- key = test_util.load_pyopenssl_private_key('rsa512_key.pem')
+ self.cert = test_util.load_comparable_cert('rsa2048_cert.pem')
+ key = test_util.load_pyopenssl_private_key('rsa2048_key.pem')
# pylint: disable=protected-access
certs = {b'foo': (key, self.cert.wrapped)}
@@ -67,6 +69,8 @@ class SSLSocketAndProbeSNITest(unittest.TestCase):
class PyOpenSSLCertOrReqSANTest(unittest.TestCase):
"""Test for acme.crypto_util._pyopenssl_cert_or_req_san."""
+ _multiprocess_can_split_ = True
+
@classmethod
def _call(cls, loader, name):
# pylint: disable=protected-access
@@ -131,6 +135,8 @@ class PyOpenSSLCertOrReqSANTest(unittest.TestCase):
class RandomSnTest(unittest.TestCase):
"""Test for random certificate serial numbers."""
+ _multiprocess_can_split_ = True
+
def setUp(self):
self.cert_count = 5
self.serial_num = []
diff --git a/acme/acme/jose/json_util.py b/acme/acme/jose/json_util.py
index da38b55ba..cc66d77ff 100644
--- a/acme/acme/jose/json_util.py
+++ b/acme/acme/jose/json_util.py
@@ -253,10 +253,6 @@ class JSONObjectWithFields(util.ImmutableMap, interfaces.JSONDeSerializable):
raise errors.SerializationError(
'Could not encode {0} ({1}): {2}'.format(
slot, value, error))
- if omitted:
- # pylint: disable=star-args
- logger.debug('Omitted empty fields: %s', ', '.join(
- '{0!s}={1!r}'.format(*field) for field in omitted))
return jobj
def to_partial_json(self):
diff --git a/acme/acme/messages.py b/acme/acme/messages.py
index a7c86a10c..29d719684 100644
--- a/acme/acme/messages.py
+++ b/acme/acme/messages.py
@@ -469,3 +469,4 @@ class Revocation(jose.JSONObjectWithFields):
resource = fields.Resource(resource_type)
certificate = jose.Field(
'certificate', decoder=jose.decode_cert, encoder=jose.encode_cert)
+ reason = jose.Field('reason')
diff --git a/acme/acme/standalone_test.py b/acme/acme/standalone_test.py
index 85cd9d11d..58469d470 100644
--- a/acme/acme/standalone_test.py
+++ b/acme/acme/standalone_test.py
@@ -21,6 +21,8 @@ from acme import test_util
class TLSServerTest(unittest.TestCase):
"""Tests for acme.standalone.TLSServer."""
+ _multiprocess_can_split_ = True
+
def test_bind(self): # pylint: disable=no-self-use
from acme.standalone import TLSServer
server = TLSServer(
@@ -31,10 +33,12 @@ class TLSServerTest(unittest.TestCase):
class TLSSNI01ServerTest(unittest.TestCase):
"""Test for acme.standalone.TLSSNI01Server."""
+ _multiprocess_can_split_ = True
+
def setUp(self):
self.certs = {b'localhost': (
- test_util.load_pyopenssl_private_key('rsa512_key.pem'),
- test_util.load_cert('cert.pem'),
+ test_util.load_pyopenssl_private_key('rsa2048_key.pem'),
+ test_util.load_cert('rsa2048_cert.pem'),
)}
from acme.standalone import TLSSNI01Server
self.server = TLSSNI01Server(("", 0), certs=self.certs)
@@ -57,6 +61,8 @@ class TLSSNI01ServerTest(unittest.TestCase):
class HTTP01ServerTest(unittest.TestCase):
"""Tests for acme.standalone.HTTP01Server."""
+ _multiprocess_can_split_ = True
+
def setUp(self):
self.account_key = jose.JWK.load(
test_util.load_vector('rsa1024_key.pem'))
@@ -109,13 +115,16 @@ class HTTP01ServerTest(unittest.TestCase):
class TestSimpleTLSSNI01Server(unittest.TestCase):
"""Tests for acme.standalone.simple_tls_sni_01_server."""
+ _multiprocess_can_split_ = True
+
def setUp(self):
# mirror ../examples/standalone
self.test_cwd = tempfile.mkdtemp()
localhost_dir = os.path.join(self.test_cwd, 'localhost')
os.makedirs(localhost_dir)
- shutil.copy(test_util.vector_path('cert.pem'), localhost_dir)
- shutil.copy(test_util.vector_path('rsa512_key.pem'),
+ shutil.copy(test_util.vector_path('rsa2048_cert.pem'),
+ os.path.join(localhost_dir, 'cert.pem'))
+ shutil.copy(test_util.vector_path('rsa2048_key.pem'),
os.path.join(localhost_dir, 'key.pem'))
from acme.standalone import simple_tls_sni_01_server
@@ -147,7 +156,8 @@ class TestSimpleTLSSNI01Server(unittest.TestCase):
time.sleep(1) # wait until thread starts
else:
self.assertEqual(jose.ComparableX509(cert),
- test_util.load_comparable_cert('cert.pem'))
+ test_util.load_comparable_cert(
+ 'rsa2048_cert.pem'))
break
diff --git a/acme/acme/testdata/rsa2048_cert.pem b/acme/acme/testdata/rsa2048_cert.pem
new file mode 100644
index 000000000..3944cd1db
--- /dev/null
+++ b/acme/acme/testdata/rsa2048_cert.pem
@@ -0,0 +1,22 @@
+-----BEGIN CERTIFICATE-----
+MIIDjjCCAnagAwIBAgIJALVG/VbBb5U7MA0GCSqGSIb3DQEBCwUAMFsxCzAJBgNV
+BAYTAkFVMQswCQYDVQQIDAJXQTEeMBwGA1UEBwwVVGhlIG1pZGRsZSBvZiBub3do
+ZXJlMR8wHQYDVQQKDBZDZXJ0Ym90IFRlc3QgQ2VydHMgSW5jMCAXDTE2MTEyODIx
+MzUzN1oYDzIyOTAwOTEzMjEzNTM3WjBbMQswCQYDVQQGEwJBVTELMAkGA1UECAwC
+V0ExHjAcBgNVBAcMFVRoZSBtaWRkbGUgb2Ygbm93aGVyZTEfMB0GA1UECgwWQ2Vy
+dGJvdCBUZXN0IENlcnRzIEluYzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC
+ggEBANoVT1pdvRUUBOqvm7M2ebLEHV7higUH7qAGUZEkfP6W4YriYVY+IHrH1svN
+PSa+oPTK7weDNmT11ehWnGyECIM9z2r2Hi9yVV0ycxh4hWQ4Nt8BAKZwCwaXpyWm
+7Gj6m2EzpSN5Dd67g5YAQBrUUh1+RRbFi9c0Ls/6ZOExMvfg8kqt4c2sXCgH1IFn
+xvvOjBYop7xh0x3L1Akyax0tw8qgQp/z5mkupmVDNJYPFmbzFPMNyDR61ed6QUTD
+g7P4UAuFkejLLzFvz5YaO7vC+huaTuPhInAhpzqpr4yU97KIjos2/83Itu/Cv8U1
+RAeEeRTkh0WjUfltoem/5f8bIdsCAwEAAaNTMFEwHQYDVR0OBBYEFHy+bEYqwvFU
+uQLTkIfQ36AM2DQiMB8GA1UdIwQYMBaAFHy+bEYqwvFUuQLTkIfQ36AM2DQiMA8G
+A1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAH3ANVzB59FcunZV/F8T
+RiCD6/gV7Jc3CswU8N8tVjzMCg2jOdTFF9iYZzNNKQvG13o/n5LkQr/lkKRQkWTx
+nkE5WZbR7vNqlzXgPa9NBiK5rPjgSt8azPW+Skct3Bj4B3PhTMSpoQ7PsUJ8UeV8
+kTNR5xrRLt6/mLfRJTXWXBM43GEZi8lL5q0nqz0tPGISADshHMo6ZlUu5Hvfp5v+
+aonpO4sVS9hGOVxjGNMXYApEUy4jid9jjAfEk6jeELJMbXGLy/botFgIJK/QPe6P
+AfbdFgtg/qzG7Uy0A1iXXfWdgwmVrhCoGYYWCn4yWCAm894QKtdim87CHSDP0WUf
+Esg=
+-----END CERTIFICATE-----
diff --git a/acme/acme/testdata/rsa2048_key.pem b/acme/acme/testdata/rsa2048_key.pem
index 33efd3467..5847aed55 100644
--- a/acme/acme/testdata/rsa2048_key.pem
+++ b/acme/acme/testdata/rsa2048_key.pem
@@ -1,27 +1,28 @@
------BEGIN RSA PRIVATE KEY-----
-MIIEowIBAAKCAQEA8HwZMHeImB/iM8/n8CTCR4KeYQB2gLGO3v8xLms+PWH3Zbxc
-dVtEn25Y34scIh+iOuEXBcSBalBddLHKBGVN3nCfmpupoLm52xgRG44q9OWODpg4
-FSi4afqVw2agMx0RHi0v3GVcdpqB83UW42kK1ESZHUuq7mxLg8u3IMYZFm6Amsf+
-YQjBbDNn8NczJOFhsExP2EdM5ykgM1Om8aqTqqPMgPub68/r4Sym+BjLnvRq5Qtz
-h/jCfOBIIpAwg3lj7l8OyE3kkD3ALtuiuminNUqLHEkUaLq/Xiv8V8mvnrhG7h3Q
-+L1Xc707P0dz5YM5XxTMhmUE1cae/lQ0KbNrpwIDAQABAoIBAAiDXCDrGlrIRimv
-YnaN1pLRfOnSKl/D6VrbjdIm2b0yip9/W4aMBJHgRiUjt4s9s3CCJ1585lftIGHR
-KWWecHM/aWb/u7GE4Z9v6qsfDUY+GhlKKjIVjvGxfTu9lk446TI4R0l2DR/luFP2
-ASlrvoZlJ0ZyN0rZapLv0zvFx32Tukd+3rcMmXfHl7aRGMZG1YTKNmBJ4d9iJ6cP
-HG3fgSzLQMPLNO/20MzbXdREG5FNQtwaMuFnIcVbtMCvc/71lQQEfANMLCUweEed
-YWGOjgDeh+731nJsopel+2TSTgnf5VhcFrgChZZdqeKvP+HbXjTE2VkWo7BrzoM7
-xICYBwECgYEA/ZF/JOjZfwIMUdikv8vldJzRMdFryl4NJWnh4NeThNOEpUcpxnyK
-wyMnnQaGJa51u9EEnzl0sZ2h2ODjD6KFpz6fkWaVRq5SWalVPAoKZGaoPZV3IUOI
-8Tm0xkXho+A/FUUEcxCLME+3V9EdPfHaVRJOrbfDyxvNhsj4w9F0aAkCgYEA8sp7
-XTrolOknJGv4Qt1w6gcm5+cMtLaRfi8ZHPHujl2x9eWE8/s2818az7jc0Xr/G4HQ
-NeU+3Es4BblEckSHmhUZhx26cZgkLSIIDofEtaEc6u8CyWfxsWvn3l4T3kMdeSLC
-9UoLk59AH2tkMIh8vzV8LSisLJa341lMdgryQi8CgYAlJKr7PSCe+i3Tz2hSsAts
-iYwbQBIKErzaPihYRzvUuSc1DreP26535y5mUg5UdrnISVXj/Qaa/fw3SLn6EFSD
-qyi0o9I6CE8H00YpBU+AZYk/fCV3Oe1VaJ6SbKog1zhmZTXBpSq+aO7ybi9aY5MX
-4xajW8fSeMAifk3yYTwsAQKBgErcEcOCOVpItU/uloKPYpRWFjHktK83p46fmP+q
-vOJak1d9KExOBfhuN4caucNBSE1D7l3fzE0CSEjDgg41gRYKMW/Ow8DopybfWlqY
-lBdokNEDVvmgug35dmnC2h9q1DiYdkJJTV57+Lp3U1H/k28lX59Q7h1lb1eDHic7
-YszzAoGBAOx05dhOiYbzAJSTQu3oBHFn4mTYIqCcDO6cQrEJwPKAq7mAhT0yOk9N
-CrqRV/1aes665829cyTwcAZl6nqbzHv5XjX5+g6vmooCb4oCkq49rumHjoQdrX8D
-RR5b+Spkc1jo4rctCcExzSkgo+K5N3oBVYznecje7O7Z0/qiJE/8
------END RSA PRIVATE KEY-----
+-----BEGIN PRIVATE KEY-----
+MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDaFU9aXb0VFATq
+r5uzNnmyxB1e4YoFB+6gBlGRJHz+luGK4mFWPiB6x9bLzT0mvqD0yu8HgzZk9dXo
+VpxshAiDPc9q9h4vclVdMnMYeIVkODbfAQCmcAsGl6clpuxo+pthM6UjeQ3eu4OW
+AEAa1FIdfkUWxYvXNC7P+mThMTL34PJKreHNrFwoB9SBZ8b7zowWKKe8YdMdy9QJ
+MmsdLcPKoEKf8+ZpLqZlQzSWDxZm8xTzDcg0etXnekFEw4Oz+FALhZHoyy8xb8+W
+Gju7wvobmk7j4SJwIac6qa+MlPeyiI6LNv/NyLbvwr/FNUQHhHkU5IdFo1H5baHp
+v+X/GyHbAgMBAAECggEAURFe4C68XRuGAF+rN2Fmt+djK6QXlGswb1gp9hRkSpd3
+3BLvMAoENOAYnsX6l26Bkr3lQRurmrgv/iBEIaqrJ25QrmgzLFwKE4zvcAdNPsYO
+z7MltLktwBOb1MlKVHPkUqvKFXeoikWWUqphKhgHNmN7900UALmrNTDVU0jgs3fB
+o35o8d5SjoC52K4wCTjhPyjt4cdbfbziRs2qFhfGdawidRO1xLlDM4tTTW+5yWGK
+lt0SwyvDVC6XWeNoT3nXyKjXWP7hcYqm0iS7ffL9YzEC2RXNGQUqeR50i9Y0rDdH
+Vqcr+Rqio2ww68zbDWBpC/jU133BSoHuSE1wstxIkQKBgQDxlEr42WJfgdajbZ1a
+hUIeLEgvhezLmD1hcYwZuQCLgizmY2ovvmeAH74koCDEsUUQunPYHsRla7wT3q1/
+IkR1KgJPwESpkQaKuAqxeEAkv7Gn8Lzcn22jCoRCfGA68wKJz2ECFZDc0RDvRrT/
+9GhiiGUoO47jv9ezrSDO1eu5/QKBgQDnGfYVMNLiA0fy4AxSyY2vdo7vruOFGpRP
+n94gwxZ+0dQDWHzn3J4rHivxtcyd/MOZv4I8PtYK7tmmjYv1ngQ6sGl4p8bpUtwj
+9++/B1CyB1W5/VPqMkd+Sj0dbejycME55+F6/r4basPXxBFFCfknjAlVvyvbBhUy
+ftNpHxZGtwKBgChJM4t2LPqCW3nbgL8ks9b2SX9rVQbKt4m1dsifWmDpb3VoJMAb
+f4UVRg8ziONkMIFOppzm3JeRNMcXflVSMJpdTA9in9CrN60QbfAUfpXiRc0cz1H3
+YEAtM8smlKGf/s9efu3rDMJWNv3AC9UXPAUae8wOypBeYKk8+NilQe89AoGAXEA3
+xFO+CqyGnwQixzVf0qf//NuSRQLMK1DEyc02gJ9gA4niKmgd11Zu8kjBClvo9MnG
+wifPJ4Qa6+pa8UwHoinjoF9Q/rit2cnSMS5JXxegd+MRCU7SzS3zYXkLYSPzbhsL
+Hh7sYmNnFA1XW3jUtZ2n6EusxPyTn5mS6MaZDNcCgYBelFKFjNIQ50NbOnm8DewK
+jUd5OFKowKodlQVcHiF9CVbjvpN8ZPRcBSmqDU4kpT/rmcybVoL6Zfa/zWkw8+Oh
+QxKb3BYf5vRUMd/RA+/t5KG4ZOIIYB3qoltAYlhVaINukL6cGVG1qvV/ntcsfsn6
+kmf1UgGFcKrJuXgwEtTVxw==
+-----END PRIVATE KEY-----
diff --git a/acme/docs/conf.py b/acme/docs/conf.py
index 55f5eee3f..dea23a8ca 100644
--- a/acme/docs/conf.py
+++ b/acme/docs/conf.py
@@ -39,7 +39,6 @@ extensions = [
'sphinx.ext.todo',
'sphinx.ext.coverage',
'sphinx.ext.viewcode',
- 'sphinxcontrib.programoutput',
]
autodoc_member_order = 'bysource'
diff --git a/acme/docs/jws-help.txt b/acme/docs/jws-help.txt
new file mode 100644
index 000000000..34cf5ce23
--- /dev/null
+++ b/acme/docs/jws-help.txt
@@ -0,0 +1,8 @@
+usage: jws [-h] [--compact] {sign,verify} ...
+
+positional arguments:
+ {sign,verify}
+
+optional arguments:
+ -h, --help show this help message and exit
+ --compact
diff --git a/acme/docs/man/jws.rst b/acme/docs/man/jws.rst
index fb3121a96..d7ff8f405 100644
--- a/acme/docs/man/jws.rst
+++ b/acme/docs/man/jws.rst
@@ -1 +1 @@
-.. program-output:: jws --help all
+.. literalinclude:: ../jws-help.txt
diff --git a/acme/examples/standalone/localhost/cert.pem b/acme/examples/standalone/localhost/cert.pem
index 569366af9..1cca87af5 120000
--- a/acme/examples/standalone/localhost/cert.pem
+++ b/acme/examples/standalone/localhost/cert.pem
@@ -1 +1 @@
-../../../acme/testdata/cert.pem
\ No newline at end of file
+../../../acme/testdata/rsa2048_cert.pem
\ No newline at end of file
diff --git a/acme/examples/standalone/localhost/key.pem b/acme/examples/standalone/localhost/key.pem
index 870f4f876..ee3dd2847 120000
--- a/acme/examples/standalone/localhost/key.pem
+++ b/acme/examples/standalone/localhost/key.pem
@@ -1 +1 @@
-../../../acme/testdata/rsa512_key.pem
\ No newline at end of file
+../../../acme/testdata/rsa2048_key.pem
\ No newline at end of file
diff --git a/acme/setup.py b/acme/setup.py
index 2b32f7e28..f325b47a2 100644
--- a/acme/setup.py
+++ b/acme/setup.py
@@ -4,20 +4,18 @@ from setuptools import setup
from setuptools import find_packages
-version = '0.10.0.dev0'
+version = '0.11.0.dev0'
# Please update tox.ini when modifying dependency version requirements
install_requires = [
# load_pem_private/public_key (>=0.6)
# rsa_recover_prime_factors (>=0.8)
'cryptography>=0.8',
- 'ndg-httpsclient', # urllib3 InsecurePlatformWarning (#304)
- 'pyasn1', # urllib3 InsecurePlatformWarning (#304)
# Connection.set_tlsext_host_name (>=0.13)
'PyOpenSSL>=0.13',
'pyrfc3339',
'pytz',
- 'requests',
+ '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',
@@ -49,7 +47,6 @@ dev_extras = [
docs_extras = [
'Sphinx>=1.0', # autodoc_member_order = 'bysource', autodoc_default_flags
'sphinx_rtd_theme',
- 'sphinxcontrib-programoutput',
]
diff --git a/certbot-apache/certbot_apache/augeas_configurator.py b/certbot-apache/certbot_apache/augeas_configurator.py
index 6999120d6..3735284ef 100644
--- a/certbot-apache/certbot_apache/augeas_configurator.py
+++ b/certbot-apache/certbot_apache/augeas_configurator.py
@@ -68,9 +68,12 @@ class AugeasConfigurator(common.Plugin):
# As aug.get may return null
if lens_path and lens in lens_path:
msg = (
- "There has been an error in parsing the file (%s): %s",
+ "There has been an error in parsing the file {0} on line {1}: "
+ "{2}".format(
# Strip off /augeas/files and /error
- path[13:len(path) - 6], self.aug.get(path + "/message"))
+ path[13:len(path) - 6],
+ self.aug.get(path + "/line"),
+ self.aug.get(path + "/message")))
raise errors.PluginError(msg)
# TODO: Cleanup this function
diff --git a/certbot-apache/certbot_apache/configurator.py b/certbot-apache/certbot_apache/configurator.py
index 75fbe3456..27e214362 100644
--- a/certbot-apache/certbot_apache/configurator.py
+++ b/certbot-apache/certbot_apache/configurator.py
@@ -1,6 +1,7 @@
"""Apache Configuration based off of Augeas Configurator."""
# pylint: disable=too-many-lines
import filecmp
+import fnmatch
import logging
import os
import re
@@ -362,18 +363,24 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
return vhost
def included_in_wildcard(self, names, target_name):
- """Helper function to see if alias is covered by wildcard"""
- target_name = target_name.split(".")[::-1]
- wildcards = [domain.split(".")[1:] for domain in
- names if domain.startswith("*")]
- for wildcard in wildcards:
- if len(wildcard) > len(target_name):
- continue
- for idx, segment in enumerate(wildcard[::-1]):
- if segment != target_name[idx]:
- break
- else:
- # https://docs.python.org/2/tutorial/controlflow.html#break-and-continue-statements-and-else-clauses-on-loops
+ """Is target_name covered by a wildcard?
+
+ :param names: server aliases
+ :type names: `collections.Iterable` of `str`
+ :param str target_name: name to compare with wildcards
+
+ :returns: True if target_name is covered by a wildcard,
+ otherwise, False
+ :rtype: bool
+
+ """
+ # use lowercase strings because fnmatch can be case sensitive
+ target_name = target_name.lower()
+ for name in names:
+ name = name.lower()
+ # fnmatch treats "[seq]" specially and [ or ] characters aren't
+ # valid in Apache but Apache doesn't error out if they are present
+ if "[" not in name and fnmatch.fnmatch(target_name, name):
return True
return False
@@ -463,7 +470,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
zope.component.getUtility(interfaces.IDisplay).notification(
"Apache mod_macro seems to be in use in file(s):\n{0}"
"\n\nUnfortunately mod_macro is not yet supported".format(
- "\n ".join(vhost_macro)))
+ "\n ".join(vhost_macro)), force_interactive=True)
return all_names
@@ -1012,6 +1019,8 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
self.parser.find_dir("ServerAlias", target_name,
start=vh_path, exclude=False)):
return
+ if self._has_matching_wildcard(vh_path, target_name):
+ return
if not self.parser.find_dir("ServerName", None,
start=vh_path, exclude=False):
self.parser.add_dir(vh_path, "ServerName", target_name)
@@ -1019,6 +1028,22 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
self.parser.add_dir(vh_path, "ServerAlias", target_name)
self._add_servernames(vhost)
+ def _has_matching_wildcard(self, vh_path, target_name):
+ """Is target_name already included in a wildcard in the vhost?
+
+ :param str vh_path: Augeas path to the vhost
+ :param str target_name: name to compare with wildcards
+
+ :returns: True if there is a wildcard covering target_name in
+ the vhost in vhost_path, otherwise, False
+ :rtype: bool
+
+ """
+ matches = self.parser.find_dir(
+ "ServerAlias", start=vh_path, exclude=False)
+ aliases = (self.aug.get(match) for match in matches)
+ return self.included_in_wildcard(aliases, target_name)
+
def _add_name_vhost_if_necessary(self, vhost):
"""Add NameVirtualHost Directives if necessary for new vhost.
@@ -1494,38 +1519,6 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
return redirects
- def get_all_certs_keys(self):
- """Find all existing keys, certs from configuration.
-
- Retrieve all certs and keys set in VirtualHosts on the Apache server
-
- :returns: list of tuples with form [(cert, key, path)]
- cert - str path to certificate file
- key - str path to associated key file
- path - File path to configuration file.
- :rtype: list
-
- """
- c_k = set()
-
- for vhost in self.vhosts:
- if vhost.ssl:
- cert_path = self.parser.find_dir(
- "SSLCertificateFile", None,
- start=vhost.path, exclude=False)
- key_path = self.parser.find_dir(
- "SSLCertificateKeyFile", None,
- start=vhost.path, exclude=False)
-
- if cert_path and key_path:
- cert = os.path.abspath(self.parser.get_arg(cert_path[-1]))
- key = os.path.abspath(self.parser.get_arg(key_path[-1]))
- c_k.add((cert, key, get_file_path(cert_path[-1])))
- else:
- logger.warning(
- "Invalid VirtualHost configuration - %s", vhost.filep)
- return c_k
-
def is_site_enabled(self, avail_fp):
"""Checks to see if the given site is enabled.
diff --git a/certbot-apache/certbot_apache/display_ops.py b/certbot-apache/certbot_apache/display_ops.py
index d7b76f83d..22aafc0fe 100644
--- a/certbot-apache/certbot_apache/display_ops.py
+++ b/certbot-apache/certbot_apache/display_ops.py
@@ -85,7 +85,8 @@ 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")
+ choices, help_label="More Info",
+ ok_label="Select", 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 "
@@ -101,4 +102,4 @@ 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)),
- height=display_util.HEIGHT)
+ force_interactive=True)
diff --git a/certbot-apache/certbot_apache/tests/augeas_configurator_test.py b/certbot-apache/certbot_apache/tests/augeas_configurator_test.py
index c55f27ff0..66da017ec 100644
--- a/certbot-apache/certbot_apache/tests/augeas_configurator_test.py
+++ b/certbot-apache/certbot_apache/tests/augeas_configurator_test.py
@@ -13,6 +13,8 @@ from certbot_apache.tests import util
class AugeasConfiguratorTest(util.ApacheTest):
"""Test for Augeas Configurator base class."""
+ _multiprocess_can_split_ = True
+
def setUp(self): # pylint: disable=arguments-differ
super(AugeasConfiguratorTest, self).setUp()
diff --git a/certbot-apache/certbot_apache/tests/configurator_test.py b/certbot-apache/certbot_apache/tests/configurator_test.py
index dc953174e..065761496 100644
--- a/certbot-apache/certbot_apache/tests/configurator_test.py
+++ b/certbot-apache/certbot_apache/tests/configurator_test.py
@@ -13,6 +13,7 @@ from certbot import achallenges
from certbot import errors
from certbot.tests import acme_util
+from certbot.tests import util as certbot_util
from certbot_apache import configurator
from certbot_apache import parser
@@ -24,6 +25,8 @@ from certbot_apache.tests import util
class MultipleVhostsTest(util.ApacheTest):
"""Test two standard well-configured HTTP vhosts."""
+ _multiprocess_can_split_ = True
+
def setUp(self): # pylint: disable=arguments-differ
super(MultipleVhostsTest, self).setUp()
@@ -95,7 +98,7 @@ class MultipleVhostsTest(util.ApacheTest):
# Weak test..
ApacheConfigurator.add_parser_arguments(mock.MagicMock())
- @mock.patch("zope.component.getUtility")
+ @certbot_util.patch_get_utility()
def test_get_all_names(self, mock_getutility):
mock_getutility.notification = mock.MagicMock(return_value=True)
names = self.config.get_all_names()
@@ -103,7 +106,7 @@ class MultipleVhostsTest(util.ApacheTest):
["certbot.demo", "ocspvhost.com", "encryption-example.demo",
"ip-172-30-0-17", "*.blue.purple.com"]))
- @mock.patch("zope.component.getUtility")
+ @certbot_util.patch_get_utility()
@mock.patch("certbot_apache.configurator.socket.gethostbyaddr")
def test_get_all_names_addrs(self, mock_gethost, mock_getutility):
mock_gethost.side_effect = [("google.com", "", ""), socket.error]
@@ -218,10 +221,6 @@ class MultipleVhostsTest(util.ApacheTest):
self.assertRaises(
errors.PluginError, self.config.choose_vhost, "none.com")
- def test_choosevhost_select_vhost_with_wildcard(self):
- chosen_vhost = self.config.choose_vhost("blue.purple.com", temp=True)
- self.assertEqual(self.vh_truth[6], chosen_vhost)
-
def test_findbest_continues_on_short_domain(self):
# pylint: disable=protected-access
chosen_vhost = self.config._find_best_vhost("purple.com")
@@ -775,21 +774,6 @@ class MultipleVhostsTest(util.ApacheTest):
self.assertRaises(errors.MisconfigurationError,
self.config.config_test)
- def test_get_all_certs_keys(self):
- c_k = self.config.get_all_certs_keys()
- self.assertEqual(len(c_k), 3)
- cert, key, path = next(iter(c_k))
- self.assertTrue("cert" in cert)
- self.assertTrue("key" in key)
- self.assertTrue("default-ssl" in path or "ocsp-ssl" in path)
-
- def test_get_all_certs_keys_malformed_conf(self):
- self.config.parser.find_dir = mock.Mock(
- side_effect=[["path"], [], ["path"], [], ["path"], []])
- c_k = self.config.get_all_certs_keys()
-
- self.assertFalse(c_k)
-
def test_more_info(self):
self.assertTrue(self.config.more_info())
@@ -1134,7 +1118,7 @@ class MultipleVhostsTest(util.ApacheTest):
not_rewriterule = "NotRewriteRule ^ ..."
self.assertFalse(self.config._sift_rewrite_rule(not_rewriterule))
- @mock.patch("certbot_apache.configurator.zope.component.getUtility")
+ @certbot_util.patch_get_utility()
def test_make_vhost_ssl_with_existing_rewrite_rule(self, mock_get_utility):
self.config.parser.modules.add("rewrite_module")
@@ -1163,7 +1147,7 @@ class MultipleVhostsTest(util.ApacheTest):
mock_get_utility().add_message.assert_called_once_with(mock.ANY,
mock.ANY)
- @mock.patch("certbot_apache.configurator.zope.component.getUtility")
+ @certbot_util.patch_get_utility()
def test_make_vhost_ssl_with_existing_rewrite_conds(self, mock_get_utility):
self.config.parser.modules.add("rewrite_module")
@@ -1256,6 +1240,7 @@ class MultipleVhostsTest(util.ApacheTest):
class AugeasVhostsTest(util.ApacheTest):
"""Test vhosts with illegal names dependant on augeas version."""
# pylint: disable=protected-access
+ _multiprocess_can_split_ = True
def setUp(self): # pylint: disable=arguments-differ
td = "debian_apache_2_4/augeas_vhosts"
@@ -1267,8 +1252,6 @@ class AugeasVhostsTest(util.ApacheTest):
self.config = util.get_apache_configurator(
self.config_path, self.vhost_path, self.config_dir, self.work_dir)
- self.vh_truth = util.get_vh_truth(
- self.temp_dir, "debian_apache_2_4/augeas_vhosts")
def tearDown(self):
shutil.rmtree(self.temp_dir)
@@ -1293,5 +1276,41 @@ class AugeasVhostsTest(util.ApacheTest):
vhs = self.config.get_virtual_hosts()
self.assertEqual([], vhs)
+ def test_choose_vhost_with_matching_wildcard(self):
+ names = (
+ "an.example.net", "another.example.net", "an.other.example.net")
+ for name in names:
+ self.assertFalse(name in self.config.choose_vhost(name).aliases)
+
+ def test_choose_vhost_without_matching_wildcard(self):
+ mock_path = "certbot_apache.display_ops.select_vhost"
+ with mock.patch(mock_path, lambda _, vhosts: vhosts[0]):
+ for name in ("a.example.net", "other.example.net"):
+ self.assertTrue(name in self.config.choose_vhost(name).aliases)
+
+ def test_choose_vhost_wildcard_not_found(self):
+ mock_path = "certbot_apache.display_ops.select_vhost"
+ names = (
+ "abc.example.net", "not.there.tld", "aa.wildcard.tld"
+ )
+ with mock.patch(mock_path) as mock_select:
+ mock_select.return_value = self.config.vhosts[0]
+ for name in names:
+ orig_cc = mock_select.call_count
+ self.config.choose_vhost(name)
+ self.assertEqual(mock_select.call_count - orig_cc, 1)
+
+ def test_choose_vhost_wildcard_found(self):
+ mock_path = "certbot_apache.display_ops.select_vhost"
+ names = (
+ "ab.example.net", "a.wildcard.tld", "yetanother.example.net"
+ )
+ with mock.patch(mock_path) as mock_select:
+ mock_select.return_value = self.config.vhosts[0]
+ for name in names:
+ self.config.choose_vhost(name)
+ self.assertEqual(mock_select.call_count, 0)
+
+
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 585661c7f..ec6eee3f2 100644
--- a/certbot-apache/certbot_apache/tests/display_ops_test.py
+++ b/certbot-apache/certbot_apache/tests/display_ops_test.py
@@ -1,12 +1,13 @@
"""Test certbot_apache.display_ops."""
-import sys
import unittest
import mock
-import zope.component
+
+from certbot import errors
from certbot.display import util as display_util
-from certbot import errors
+
+from certbot.tests import util as certbot_util
from certbot_apache import obj
@@ -17,7 +18,6 @@ class SelectVhostTest(unittest.TestCase):
"""Tests for certbot_apache.display_ops.select_vhost."""
def setUp(self):
- zope.component.provideUtility(display_util.FileDisplay(sys.stdout))
self.base_dir = "/example_path"
self.vhosts = util.get_vh_truth(
self.base_dir, "debian_apache_2_4/multiple_vhosts")
@@ -27,12 +27,12 @@ class SelectVhostTest(unittest.TestCase):
from certbot_apache.display_ops import select_vhost
return select_vhost("example.com", vhosts)
- @mock.patch("certbot_apache.display_ops.zope.component.getUtility")
+ @certbot_util.patch_get_utility()
def test_successful_choice(self, mock_util):
mock_util().menu.return_value = (display_util.OK, 3)
self.assertEqual(self.vhosts[3], self._call(self.vhosts))
- @mock.patch("certbot_apache.display_ops.zope.component.getUtility")
+ @certbot_util.patch_get_utility()
def test_noninteractive(self, mock_util):
mock_util().menu.side_effect = errors.MissingCommandlineFlag("no vhost default")
try:
@@ -40,7 +40,7 @@ class SelectVhostTest(unittest.TestCase):
except errors.MissingCommandlineFlag as e:
self.assertTrue("vhost ambiguity" in e.message)
- @mock.patch("certbot_apache.display_ops.zope.component.getUtility")
+ @certbot_util.patch_get_utility()
def test_more_info_cancel(self, mock_util):
mock_util().menu.side_effect = [
(display_util.HELP, 1),
@@ -55,7 +55,7 @@ class SelectVhostTest(unittest.TestCase):
self.assertEqual(self._call([]), None)
@mock.patch("certbot_apache.display_ops.display_util")
- @mock.patch("certbot_apache.display_ops.zope.component.getUtility")
+ @certbot_util.patch_get_utility()
@mock.patch("certbot_apache.display_ops.logger")
def test_small_display(self, mock_logger, mock_util, mock_display_util):
mock_display_util.WIDTH = 20
@@ -64,7 +64,7 @@ class SelectVhostTest(unittest.TestCase):
self.assertEqual(mock_logger.debug.call_count, 1)
- @mock.patch("certbot_apache.display_ops.zope.component.getUtility")
+ @certbot_util.patch_get_utility()
def test_multiple_names(self, mock_util):
mock_util().menu.return_value = (display_util.OK, 5)
diff --git a/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/sites-available/another_wildcard.conf b/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/sites-available/another_wildcard.conf
new file mode 100644
index 000000000..1a5b7de47
--- /dev/null
+++ b/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/sites-available/another_wildcard.conf
@@ -0,0 +1,11 @@
+
+ ServerName wildcard.tld
+ ServerAlias ?.wildcard.tld
+ ServerAdmin webmaster@localhost
+ DocumentRoot /var/www/html
+
+ ErrorLog ${APACHE_LOG_DIR}/error.log
+ CustomLog ${APACHE_LOG_DIR}/access.log combined
+
+
+# vim: syntax=apache ts=4 sw=4 sts=4 sr noet
diff --git a/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/sites-available/wildcard.conf b/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/sites-available/wildcard.conf
new file mode 100644
index 000000000..b8046e6c9
--- /dev/null
+++ b/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/sites-available/wildcard.conf
@@ -0,0 +1,11 @@
+
+ ServerName example.net
+ ServerAlias ??.example.net *.other.example.net *another.example.net
+ ServerAdmin webmaster@localhost
+ DocumentRoot /var/www/html
+
+ ErrorLog ${APACHE_LOG_DIR}/error.log
+ CustomLog ${APACHE_LOG_DIR}/access.log combined
+
+
+# vim: syntax=apache ts=4 sw=4 sts=4 sr noet
diff --git a/certbot-apache/certbot_apache/tests/util.py b/certbot-apache/certbot_apache/tests/util.py
index 050876687..3c33a0e19 100644
--- a/certbot-apache/certbot_apache/tests/util.py
+++ b/certbot-apache/certbot_apache/tests/util.py
@@ -13,7 +13,7 @@ from certbot.display import util as display_util
from certbot.plugins import common
-from certbot.tests import test_util
+from certbot.tests import util as test_util
from certbot_apache import configurator
from certbot_apache import constants
@@ -64,7 +64,8 @@ class ParserTest(ApacheTest): # pytlint: disable=too-few-public-methods
vhost_root="debian_apache_2_4/multiple_vhosts/apache2/sites-available"):
super(ParserTest, self).setUp(test_dir, config_root, vhost_root)
- zope.component.provideUtility(display_util.FileDisplay(sys.stdout))
+ zope.component.provideUtility(display_util.FileDisplay(sys.stdout,
+ False))
from certbot_apache.parser import ApacheParser
self.aug = augeas.Augeas(
diff --git a/certbot-apache/setup.py b/certbot-apache/setup.py
index 2b4ac8563..d50a414a1 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.10.0.dev0'
+version = '0.11.0.dev0'
# Please update tox.ini when modifying dependency version requirements
install_requires = [
diff --git a/certbot-auto b/certbot-auto
index cba185eae..a2ddf76ac 100755
--- a/certbot-auto
+++ b/certbot-auto
@@ -15,11 +15,15 @@ set -e # Work even if somebody does "sh thisscript.sh".
# Note: you can set XDG_DATA_HOME or VENV_PATH before running this script,
# if you want to change where the virtual environment will be installed
-XDG_DATA_HOME=${XDG_DATA_HOME:-~/.local/share}
+if [ -z "$XDG_DATA_HOME" ]; then
+ XDG_DATA_HOME=~/.local/share
+fi
VENV_NAME="letsencrypt"
-VENV_PATH=${VENV_PATH:-"$XDG_DATA_HOME/$VENV_NAME"}
+if [ -z "$VENV_PATH" ]; then
+ VENV_PATH="$XDG_DATA_HOME/$VENV_NAME"
+fi
VENV_BIN="$VENV_PATH/bin"
-LE_AUTO_VERSION="0.9.3"
+LE_AUTO_VERSION="0.10.0"
BASENAME=$(basename $0)
USAGE="Usage: $BASENAME [OPTIONS]
A self-updating wrapper script for the Certbot ACME client. When run, updates
@@ -58,6 +62,7 @@ for arg in "$@" ; do
--verbose)
VERBOSE=1;;
-[!-]*)
+ OPTIND=1
while getopts ":hnvq" short_arg $arg; do
case "$short_arg" in
h)
@@ -79,43 +84,74 @@ if [ $BASENAME = "letsencrypt-auto" ]; then
HELP=0
fi
+# Support for busybox and others where there is no "command",
+# but "which" instead
+if command -v command > /dev/null 2>&1 ; then
+ export EXISTS="command -v"
+elif which which > /dev/null 2>&1 ; then
+ export EXISTS="which"
+else
+ echo "Cannot find command nor which... please install one!"
+ exit 1
+fi
+
# certbot-auto needs root access to bootstrap OS dependencies, and
# certbot itself needs root access for almost all modes of operation
# The "normal" case is that sudo is used for the steps that need root, but
# this script *can* be run as root (not recommended), or fall back to using
-# `su`
+# `su`. Auto-detection can be overrided by explicitly setting the
+# environment variable LE_AUTO_SUDO to 'sudo', 'sudo_su' or '' as used below.
+
+# Because the parameters in `su -c` has to be a string,
+# we need to properly escape it.
+su_sudo() {
+ args=""
+ # This `while` loop iterates over all parameters given to this function.
+ # For each parameter, all `'` will be replace by `'"'"'`, and the escaped string
+ # will be wrapped in a pair of `'`, then appended to `$args` string
+ # For example, `echo "It's only 1\$\!"` will be escaped to:
+ # 'echo' 'It'"'"'s only 1$!'
+ # │ │└┼┘│
+ # │ │ │ └── `'s only 1$!'` the literal string
+ # │ │ └── `\"'\"` is a single quote (as a string)
+ # │ └── `'It'`, to be concatenated with the strings following it
+ # └── `echo` wrapped in a pair of `'`, it's totally fine for the shell command itself
+ while [ $# -ne 0 ]; do
+ args="$args'$(printf "%s" "$1" | sed -e "s/'/'\"'\"'/g")' "
+ shift
+ done
+ su root -c "$args"
+}
+
SUDO_ENV=""
export CERTBOT_AUTO="$0"
-if test "`id -u`" -ne "0" ; then
- if command -v sudo 1>/dev/null 2>&1; then
- SUDO=sudo
- SUDO_ENV="CERTBOT_AUTO=$0"
- else
- echo \"sudo\" is not available, will use \"su\" for installation steps...
- # Because the parameters in `su -c` has to be a string,
- # we need properly escape it
- su_sudo() {
- args=""
- # This `while` loop iterates over all parameters given to this function.
- # For each parameter, all `'` will be replace by `'"'"'`, and the escaped string
- # will be wrapped in a pair of `'`, then appended to `$args` string
- # For example, `echo "It's only 1\$\!"` will be escaped to:
- # 'echo' 'It'"'"'s only 1$!'
- # │ │└┼┘│
- # │ │ │ └── `'s only 1$!'` the literal string
- # │ │ └── `\"'\"` is a single quote (as a string)
- # │ └── `'It'`, to be concatenated with the strings following it
- # └── `echo` wrapped in a pair of `'`, it's totally fine for the shell command itself
- while [ $# -ne 0 ]; do
- args="$args'$(printf "%s" "$1" | sed -e "s/'/'\"'\"'/g")' "
- shift
- done
- su root -c "$args"
- }
- SUDO=su_sudo
- fi
+if [ -n "${LE_AUTO_SUDO+x}" ]; then
+ case "$LE_AUTO_SUDO" in
+ su_sudo|su)
+ SUDO=su_sudo
+ ;;
+ sudo)
+ SUDO=sudo
+ SUDO_ENV="CERTBOT_AUTO=$0"
+ ;;
+ '') ;; # Nothing to do for plain root method.
+ *)
+ echo "Error: unknown root authorization mechanism '$LE_AUTO_SUDO'."
+ exit 1
+ esac
+ echo "Using preset root authorization mechanism '$LE_AUTO_SUDO'."
else
- SUDO=
+ if test "`id -u`" -ne "0" ; then
+ if $EXISTS sudo 1>/dev/null 2>&1; then
+ SUDO=sudo
+ SUDO_ENV="CERTBOT_AUTO=$0"
+ else
+ echo \"sudo\" is not available, will use \"su\" for installation steps...
+ SUDO=su_sudo
+ fi
+ else
+ SUDO=
+ fi
fi
ExperimentalBootstrap() {
@@ -136,7 +172,7 @@ ExperimentalBootstrap() {
DeterminePythonVersion() {
for LE_PYTHON in "$LE_PYTHON" python2.7 python27 python2 python; do
# Break (while keeping the LE_PYTHON value) if found.
- command -v "$LE_PYTHON" > /dev/null && break
+ $EXISTS "$LE_PYTHON" > /dev/null && break
done
if [ "$?" != "0" ]; then
echo "Cannot find any Pythons; please install one!"
@@ -177,19 +213,22 @@ BootstrapDebCommon() {
# distro version (#346)
virtualenv=
- if apt-cache show virtualenv > /dev/null 2>&1 && ! apt-cache --quiet=0 show virtualenv 2>&1 | grep -q 'No packages found'; then
- virtualenv="virtualenv"
+ # virtual env is known to apt and is installable
+ if apt-cache show virtualenv > /dev/null 2>&1 ; then
+ if ! LC_ALL=C apt-cache --quiet=0 show virtualenv 2>&1 | grep -q 'No packages found'; then
+ virtualenv="virtualenv"
+ fi
fi
if apt-cache show python-virtualenv > /dev/null 2>&1; then
- virtualenv="$virtualenv python-virtualenv"
+ virtualenv="$virtualenv python-virtualenv"
fi
augeas_pkg="libaugeas0 augeas-lenses"
- AUGVERSION=`apt-cache show --no-all-versions libaugeas0 | grep ^Version: | cut -d" " -f2`
+ AUGVERSION=`LC_ALL=C apt-cache show --no-all-versions libaugeas0 | grep ^Version: | cut -d" " -f2`
if [ "$ASSUME_YES" = 1 ]; then
- YES_FLAG="-y"
+ YES_FLAG="-y"
fi
AddBackportRepo() {
@@ -248,15 +287,15 @@ BootstrapDebCommon() {
python-dev \
$virtualenv \
gcc \
- dialog \
$augeas_pkg \
libssl-dev \
+ openssl \
libffi-dev \
ca-certificates \
- if ! command -v virtualenv > /dev/null ; then
+ if ! $EXISTS virtualenv > /dev/null ; then
echo Failed to install a working \"virtualenv\" command, exiting
exit 1
fi
@@ -307,7 +346,6 @@ BootstrapRpmCommon() {
pkgs="
gcc
- dialog
augeas-libs
openssl
openssl-devel
@@ -361,7 +399,6 @@ BootstrapSuseCommon() {
python-devel \
python-virtualenv \
gcc \
- dialog \
augeas-lenses \
libopenssl-devel \
libffi-devel \
@@ -380,7 +417,6 @@ BootstrapArchCommon() {
python2
python-virtualenv
gcc
- dialog
augeas
openssl
libffi
@@ -404,22 +440,26 @@ BootstrapGentooCommon() {
PACKAGES="
dev-lang/python:2.7
dev-python/virtualenv
- dev-util/dialog
app-admin/augeas
dev-libs/openssl
dev-libs/libffi
app-misc/ca-certificates
virtual/pkgconfig"
+ ASK_OPTION="--ask"
+ if [ "$ASSUME_YES" = 1 ]; then
+ ASK_OPTION=""
+ fi
+
case "$PACKAGE_MANAGER" in
(paludis)
$SUDO cave resolve --preserve-world --keep-targets if-possible $PACKAGES -x
;;
(pkgcore)
- $SUDO pmerge --noreplace --oneshot $PACKAGES
+ $SUDO pmerge --noreplace --oneshot $ASK_OPTION $PACKAGES
;;
(portage|*)
- $SUDO emerge --noreplace --oneshot $PACKAGES
+ $SUDO emerge --noreplace --oneshot $ASK_OPTION $PACKAGES
;;
esac
}
@@ -449,7 +489,6 @@ BootstrapMac() {
fi
$pkgcmd augeas
- $pkgcmd dialog
if [ "$(which python)" = "/System/Library/Frameworks/Python.framework/Versions/2.7/bin/python" \
-o "$(which python)" = "/usr/bin/python" ]; then
# We want to avoid using the system Python because it requires root to use pip.
@@ -458,7 +497,7 @@ BootstrapMac() {
$pkgcmd python
fi
- # Workaround for _dlopen not finding augeas on OS X
+ # Workaround for _dlopen not finding augeas on macOS
if [ "$pkgman" = "port" ] && ! [ -e "/usr/local/lib/libaugeas.dylib" ] && [ -e "/opt/local/lib/libaugeas.dylib" ]; then
echo "Applying augeas workaround"
$SUDO mkdir -p /usr/local/lib/
@@ -496,8 +535,8 @@ BootstrapMageiaCommon() {
if ! $SUDO urpmi --force \
git \
gcc \
- cdialog \
python-augeas \
+ openssl \
libopenssl-devel \
libffi-devel \
rootcerts
@@ -541,7 +580,7 @@ Bootstrap() {
elif uname | grep -iq FreeBSD ; then
ExperimentalBootstrap "FreeBSD" BootstrapFreeBsd
elif uname | grep -iq Darwin ; then
- ExperimentalBootstrap "Mac OS X" BootstrapMac
+ ExperimentalBootstrap "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
@@ -557,7 +596,7 @@ Bootstrap() {
}
TempDir() {
- mktemp -d 2>/dev/null || mktemp -d -t 'le' # Linux || OS X
+ mktemp -d 2>/dev/null || mktemp -d -t 'le' # Linux || macOS
}
@@ -594,6 +633,11 @@ if [ "$1" = "--le-auto-phase2" ]; then
# `pip install --no-cache-dir -e acme -e . -e certbot-apache -e certbot-nginx`,
# and then use `hashin` or a more secure method to gather the hashes.
+# Hashin example:
+# pip install hashin
+# hashin -r letsencrypt-auto-requirements.txt cryptography==1.5.2
+# sets the new certbot-auto pinned version of cryptography to 1.5.2
+
argparse==1.4.0 \
--hash=sha256:c31647edb69fd3d465a847ea3157d37bed1f95f19760b11a47aa91c04b666314 \
--hash=sha256:62b089a55be1d8949cd2bc7e0df0bddb9e028faefc8c32038cc84862aefdd6e4
@@ -601,7 +645,8 @@ argparse==1.4.0 \
# This comes before cffi because cffi will otherwise install an unchecked
# version via setup_requires.
pycparser==2.14 \
- --hash=sha256:7959b4a74abdc27b312fed1c21e6caf9309ce0b29ea86b591fd2e99ecdf27f73
+ --hash=sha256:7959b4a74abdc27b312fed1c21e6caf9309ce0b29ea86b591fd2e99ecdf27f73 \
+ --no-binary pycparser
cffi==1.4.2 \
--hash=sha256:53c1c9ddb30431513eb7f3cdef0a3e06b0f1252188aaa7744af0f5a4cd45dbaf \
@@ -624,29 +669,29 @@ ConfigArgParse==0.10.0 \
--hash=sha256:3b50a83dd58149dfcee98cb6565265d10b53e9c0a2bca7eeef7fb5f5524890a7
configobj==5.0.6 \
--hash=sha256:a2f5650770e1c87fb335af19a9b7eb73fc05ccf22144eb68db7d00cd2bcb0902
-cryptography==1.3.4 \
- --hash=sha256:bede00edd11a2a62c8c98c271cc103fa3a3d72acf64f6e5e4eaf251128897b17 \
- --hash=sha256:53b39e687b744bb548a98f40736cc529d9f60959b4e6cc551322cf9505d35eb3 \
- --hash=sha256:474b73ad1139b4e423e46bbd818efd0d5c0df1c65d9f7c957d64c9215d77afde \
- --hash=sha256:aaddf9592d5b99e32dd518bb4a25b147c124f9d6b4ad64b94f01b15d1666b8c8 \
- --hash=sha256:6dcad2f407db8c3cd6ecd78361439c449a4f94786b46c54507e7e68f51e1709d \
- --hash=sha256:475c153fc622e656f1f10a9c9941d0ac7ab18df7c38d35d563a437c1c0e34f24 \
- --hash=sha256:86dd61df581cba04e89e45081efbc531faff1c9d99c77b1ce97f87216c356353 \
- --hash=sha256:75cc697e4ef5fdd0102ca749114c6370dbd11db0c9132a18834858c2566247e3 \
- --hash=sha256:ea03ad5b9df6d79fc9fc1ab23729e01e1c920d2974c5e3c634ccf45a5c378452 \
- --hash=sha256:c8872b8fe4f3416d6338ab99612f49ab314f7856cb43bffab2a32d28a6267be8 \
- --hash=sha256:468fc6e16eaec6ceaa6bc341273e6e9912d01b42b740f8cf896ace7fcd6a321d \
- --hash=sha256:d6fea3c6502735011c5d61a62aef1c1d770fc6a2def45d9e6c0d94c9651e3317 \
- --hash=sha256:3cf95f179f4bead3d5649b91860ef4cf60ad4244209190fc405908272576d961 \
- --hash=sha256:141f77e60a5b9158309b2b60288c7f81d37faa15c22a69b94c190ceefaaa6236 \
- --hash=sha256:87b7a1fe703c6424451f3372d1879dae91c7fe5e13375441a72833db76fee30e \
- --hash=sha256:f5ee3cb0cf1a6550bf483ccffa6608db267a377b45f7e3a8201a86d1d8feb19f \
- --hash=sha256:4e097286651ea318300af3251375d48b71b8228481c56cd617ddd4459a1ff261 \
- --hash=sha256:1e3d3ae3f22f22d50d340f47f25227511326f3f1396c6d2446a5b45b516c4313 \
- --hash=sha256:6a057941cb64d79834ea3cf99093fcc4787c2a5d44f686c4f297361ddc419bcd \
- --hash=sha256:68b3d5390b92559ddd3353c73ab2dfcff758f9c4ec4f5d5226ccede0e5d779f4 \
- --hash=sha256:545dc003b4b6081f9c3e452da15d819b04b696f49484aff64c0a2aedf766bef8 \
- --hash=sha256:423ff890c01be7c70dbfeaa967eeef5146f1a43a5f810ffdc07b178e48a105a9
+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
enum34==1.1.2 \
--hash=sha256:2475d7fcddf5951e92ff546972758802de5260bf409319a9f1934e6bbc8b1dc7 \
--hash=sha256:35907defb0f992b75ab7788f65fedc1cf20ffa22688e0e6f6f12afc06b3ea501
@@ -662,8 +707,6 @@ ipaddress==1.0.16 \
linecache2==1.0.0 \
--hash=sha256:e78be9c0a0dfcbac712fe04fbf92b96cddae80b1b842f24248214c8496f006ef \
--hash=sha256:4b26ff4e7110db76eeb6f5a7b64a82623839d595c2038eeda662f2a2db78e97c
-ndg-httpsclient==0.4.0 \
- --hash=sha256:e8c155fdebd9c4bcb0810b4ed01ae1987554b1ee034dd7532d7b8fdae38a6274
ordereddict==1.1 \
--hash=sha256:1c35b4ac206cef2d24816c89f89cf289dd3d38cf7c449bb3fab7bf6d43f01b1f
parsedatetime==2.1 \
@@ -684,9 +727,9 @@ pyasn1==0.1.9 \
--hash=sha256:5191ff6b9126d2c039dd87f8ff025bed274baf07fa78afa46f556b1ad7265d6e \
--hash=sha256:8323e03637b2d072cc7041300bac6ec448c3c28950ab40376036788e9a1af629 \
--hash=sha256:853cacd96d1f701ddd67aa03ecc05f51890135b7262e922710112f12a2ed2a7f
-pyopenssl==16.0.0 \
- --hash=sha256:5add70cf00273bf957ca31fdb0df9b0ae4639e081897d5f86a0ae1f104901230 \
- --hash=sha256:363d10ee43d062285facf4e465f4f5163f9f702f9134f0a5896f134cbb92d17d
+pyOpenSSL==16.2.0 \
+ --hash=sha256:26ca380ddf272f7556e48064bbcd5bd71f83dfc144f3583501c7ddbd9434ee17 \
+ --hash=sha256:7779a3bbb74e79db234af6a08775568c6769b5821faecf6e2f4143edb227516e
pyparsing==2.1.8 \
--hash=sha256:2f0f5ceb14eccd5aef809d6382e87df22ca1da583c79f6db01675ce7d7f49c18 \
--hash=sha256:03a4869b9f3493807ee1f1cb405e6d576a1a2ca4d81a982677c0c1ad6177c56b \
@@ -701,9 +744,6 @@ pyRFC3339==1.0 \
--hash=sha256:8dfbc6c458b8daba1c0f3620a8c78008b323a268b27b7359e92a4ae41325f535
python-augeas==0.5.0 \
--hash=sha256:67d59d66cdba8d624e0389b87b2a83a176f21f16a87553b50f5703b23f29bac2
-python2-pythondialog==3.3.0 \
- --hash=sha256:04e93f24995c43dd90f338d5d865ca72ce3fb5a5358d4daa4965571db35fc3ec \
- --hash=sha256:3e6f593fead98f8a526bc3e306933533236e33729f552f52896ea504f55313fa
pytz==2015.7 \
--hash=sha256:3abe6a6d3fc2fbbe4c60144211f45da2edbe3182a6f6511af6bbba0598b1f992 \
--hash=sha256:939ef9c1e1224d980405689a97ffcf7828c56d1517b31d73464356c1f2b7769e \
@@ -718,9 +758,9 @@ pytz==2015.7 \
--hash=sha256:fbd26746772c24cb93c8b97cbdad5cb9e46c86bbdb1b9d8a743ee00e2fb1fc5d \
--hash=sha256:99266ef30a37e43932deec2b7ca73e83c8dbc3b9ff703ec73eca6b1dae6befea \
--hash=sha256:8b6ce1c993909783bc96e0b4f34ea223bff7a4df2c90bdb9c4e0f1ac928689e3
-requests==2.9.1 \
- --hash=sha256:113fbba5531a9e34945b7d36b33a084e8ba5d0664b703c81a7c572d91919a5b8 \
- --hash=sha256:c577815dd00f1394203fc44eb979724b098f88264a9ef898ee45b8e5e9cf587f
+requests==2.12.1 \
+ --hash=sha256:3f3f27a9d0f9092935efc78054ef324eb9f8166718270aefe036dfa1e4f68e1e \
+ --hash=sha256:2109ecea94df90980be040490ff1d879971b024861539abb00054062388b612e
six==1.10.0 \
--hash=sha256:0ff78c403d9bccf5a425a6d31a12aa6b47f1c21ca4dc2573a7e2f32a97335eb1 \
--hash=sha256:105f8d68616f8248e24bf0e9372ef04d3cc10104f1980f54d57b2ce73a5ad56a
@@ -761,18 +801,18 @@ letsencrypt==0.7.0 \
# THE LINES BELOW ARE EDITED BY THE RELEASE SCRIPT; ADD ALL DEPENDENCIES ABOVE.
-acme==0.9.3 \
- --hash=sha256:d18ce17a75ad24d27981dfaef0524aa905eab757b267e027162b56a8967ab8fb \
- --hash=sha256:a6eff1f955eb2e4316abd9aa2fedb6d9345e6b5b8a2d64ea0ad35e05d6124099
-certbot==0.9.3 \
- --hash=sha256:a87ef4c53c018df4e52ee2f2e906ad16bbb37789f29e6f284c495a2eb4d9b243 \
- --hash=sha256:68149cb8392b29f5d5246e7226d25f913f2b10482bf3bc7368e8c8821d25f3b0
-certbot-apache==0.9.3 \
- --hash=sha256:f379b1053e10709692654d7a6fcea9eaed19b66c49a753b61e31bd06a04b0aac \
- --hash=sha256:a5d98cf972072de08f984db4e6a7f20269f3f023c43f6d4e781fe43be7c10086
-certbot-nginx==0.9.3 \
- --hash=sha256:3c26f18f0b57550f069263bd9b2984ef33eab6693e7796611c1b2cc16574069c \
- --hash=sha256:7337a2e90e0b28a1ab09e31d9fb81c6d78e6453500c824c0f18bab5d31b63058
+acme==0.10.0 \
+ --hash=sha256:df4299a9881d94185a1578ed97334430a90f761ce815edd300860ca47d0538f1 \
+ --hash=sha256:ddebdf1fe139c8fedbcf633955ec867496d2f7d2d2e9879d538437a69ab47876
+certbot==0.10.0 \
+ --hash=sha256:fb1bfa3d54ce9366758e374f7ed99667ce20484224934d3e8e57839fcf784bc5 \
+ --hash=sha256:dd64ed8fb3cc3b053f05e779b934433445918668c49bcdbb2c816062815e1661
+certbot-apache==0.10.0 \
+ --hash=sha256:909d59c53507093f838f7336f75d7d78563a35b16afdf6c30f45c9f47bf069da \
+ --hash=sha256:6f110dae227dd0fea9572fa12dd60b041e391f5d2028cc2e1fedd2a9a0d2bc88
+certbot-nginx==0.10.0 \
+ --hash=sha256:4f33a230d420cbd0431e7b707fb9a1732bfd18d3c6056019591bd7c3a13abe92 \
+ --hash=sha256:c12ffd05207b0be3c765b3d3e2927e0b2cc2b7de20654b19d154a0d789e7c1d5
UNLIKELY_EOF
# -------------------------------------------------------------------------
@@ -940,7 +980,28 @@ UNLIKELY_EOF
# Report error. (Otherwise, be quiet.)
echo "Had a problem while installing Python packages."
if [ "$VERBOSE" != 1 ]; then
+ echo
+ echo "pip prints the following errors: "
+ echo "====================================================="
echo "$PIP_OUT"
+ echo "====================================================="
+ echo
+ echo "Certbot has problem setting up the virtual environment."
+
+ if `echo $PIP_OUT | grep -q Killed` || `echo $PIP_OUT | grep -q "allocate memory"` ; then
+ echo
+ echo "Based on your pip output, the problem can likely be fixed by "
+ echo "increasing the available memory."
+ else
+ echo
+ echo "We were not be able to guess the right solution from your pip "
+ echo "output."
+ fi
+
+ echo
+ echo "Consult https://certbot.eff.org/docs/install.html#problems-with-python-virtual-environment"
+ echo "for possible solutions."
+ echo "You may also find some support resources at https://certbot.eff.org/support/ ."
fi
rm -rf "$VENV_PATH"
exit 1
@@ -1132,7 +1193,7 @@ UNLIKELY_EOF
# TODO: Deal with quotes in pathnames.
echo "Replacing certbot-auto..."
# Clone permissions with cp. chmod and chown don't have a --reference
- # option on OS X or BSD, and stat -c on Linux is stat -f on OS X and BSD:
+ # option on macOS or BSD, and stat -c on Linux is stat -f on macOS and BSD:
$SUDO cp -p "$0" "$TEMP_DIR/letsencrypt-auto.permission-clone"
$SUDO cp "$TEMP_DIR/letsencrypt-auto" "$TEMP_DIR/letsencrypt-auto.permission-clone"
# Using mv rather than cp leaves the old file descriptor pointing to the
diff --git a/certbot-compatibility-test/Dockerfile b/certbot-compatibility-test/Dockerfile
index e445a3555..c8ef62696 100644
--- a/certbot-compatibility-test/Dockerfile
+++ b/certbot-compatibility-test/Dockerfile
@@ -38,8 +38,7 @@ RUN virtualenv --no-site-packages -p python2 /opt/certbot/venv && \
-e /opt/certbot/src \
-e /opt/certbot/src/certbot-apache \
-e /opt/certbot/src/certbot-nginx \
- -e /opt/certbot/src/certbot-compatibility-test \
- -e /opt/certbot/src[dev,docs]
+ -e /opt/certbot/src/certbot-compatibility-test
# install in editable mode (-e) to save space: it's not possible to
# "rm -rf /opt/certbot/src" (it's stays in the underlaying image);
diff --git a/certbot-compatibility-test/setup.py b/certbot-compatibility-test/setup.py
index 32e5935fb..fa49d9f3d 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.10.0.dev0'
+version = '0.11.0.dev0'
install_requires = [
'certbot',
diff --git a/certbot-nginx/certbot_nginx/configurator.py b/certbot-nginx/certbot_nginx/configurator.py
index bfc7b6a67..f19a07910 100644
--- a/certbot-nginx/certbot_nginx/configurator.py
+++ b/certbot-nginx/certbot_nginx/configurator.py
@@ -29,6 +29,36 @@ from certbot_nginx import parser
logger = logging.getLogger(__name__)
+REDIRECT_BLOCK = [[
+ ['\n ', 'if', ' ', '($scheme != "https") '],
+ [['\n ', 'return', ' ', '301 https://$host$request_uri'],
+ '\n ']
+], ['\n']]
+
+TEST_REDIRECT_BLOCK = [
+ [
+ ['if', '($scheme != "https")'],
+ [
+ ['return', '301 https://$host$request_uri']
+ ]
+ ],
+ ['#', ' managed by Certbot']
+]
+
+REDIRECT_COMMENT_BLOCK = [
+ ['\n ', '#', ' Redirect non-https traffic to https'],
+ ['\n ', '#', ' if ($scheme != "https") {'],
+ ['\n ', '#', " return 301 https://$host$request_uri;"],
+ ['\n ', '#', " } # managed by Certbot"],
+ ['\n']
+]
+
+TEST_REDIRECT_COMMENT_BLOCK = [
+ ['#', ' Redirect non-https traffic to https'],
+ ['#', ' if ($scheme != "https") {'],
+ ['#', " return 301 https://$host$request_uri;"],
+ ['#', " } # managed by Certbot"],
+]
@zope.interface.implementer(interfaces.IAuthenticator, interfaces.IInstaller)
@zope.interface.provider(interfaces.IPluginFactory)
@@ -161,11 +191,9 @@ class NginxConfigurator(common.Plugin):
vhost.filep, vhost.names)
except errors.MisconfigurationError as error:
logger.debug(error)
- logger.warning(
- "Cannot find a cert or key directive in %s for %s. "
- "VirtualHost was not modified.", vhost.filep, vhost.names)
# Presumably break here so that the virtualhost is not modified
- return False
+ raise errors.PluginError("Cannot find a cert or key directive in {0} for {1}. "
+ "VirtualHost was not modified.".format(vhost.filep, vhost.names))
self.save_notes += ("Changed vhost at %s with addresses of %s\n" %
(vhost.filep,
@@ -405,7 +433,8 @@ class NginxConfigurator(common.Plugin):
cert = acme_crypto_util.gen_ss_cert(key, domains=[socket.gethostname()])
cert_pem = OpenSSL.crypto.dump_certificate(
OpenSSL.crypto.FILETYPE_PEM, cert)
- cert_file, cert_path = util.unique_file(os.path.join(tmp_dir, "cert.pem"))
+ cert_file, cert_path = util.unique_file(
+ os.path.join(tmp_dir, "cert.pem"), mode="wb")
with cert_file:
cert_file.write(cert_pem)
return cert_path, le_key.file
@@ -443,18 +472,6 @@ class NginxConfigurator(common.Plugin):
self.parser.add_server_directives(
vhost, ssl_block, replace=False)
- def get_all_certs_keys(self):
- """Find all existing keys, certs from configuration.
-
- :returns: list of tuples with form [(cert, key, path)]
- cert - str path to certificate file
- key - str path to associated key file
- path - File path to configuration file.
- :rtype: set
-
- """
- return self.parser.get_all_certs_keys()
-
##################################
# enhancement methods (IInstaller)
##################################
@@ -482,6 +499,23 @@ class NginxConfigurator(common.Plugin):
logger.warning("Failed %s for %s", enhancement, domain)
raise
+ def _has_certbot_redirect(self, vhost):
+ return vhost.contains_list(TEST_REDIRECT_BLOCK)
+
+ def _has_certbot_redirect_comment(self, vhost):
+ return vhost.contains_list(TEST_REDIRECT_COMMENT_BLOCK)
+
+ def _add_redirect_block(self, vhost, active=True):
+ """Add redirect directive to vhost
+ """
+ if active:
+ redirect_block = REDIRECT_BLOCK
+ else:
+ redirect_block = REDIRECT_COMMENT_BLOCK
+
+ self.parser.add_server_directives(
+ vhost, redirect_block, replace=False)
+
def _enable_redirect(self, domain, unused_options):
"""Redirect all equivalent HTTP traffic to ssl_vhost.
@@ -504,17 +538,20 @@ class NginxConfigurator(common.Plugin):
logger.info("No matching insecure server blocks listening on port %s found.",
self.DEFAULT_LISTEN_PORT)
else:
- # Redirect plaintextish host to https
- redirect_block = [[
- ['\n ', 'if', ' ', '($scheme != "https") '],
- [['\n ', 'return', ' ', '301 https://$host$request_uri'],
- '\n ']
- ], ['\n']]
-
- self.parser.add_server_directives(
- vhost, redirect_block, replace=False)
- logger.info("Redirecting all traffic on port %s to ssl in %s",
- self.DEFAULT_LISTEN_PORT, vhost.filep)
+ if self._has_certbot_redirect(vhost):
+ logger.info("Traffic on port %s already redirecting to ssl in %s",
+ self.DEFAULT_LISTEN_PORT, vhost.filep)
+ elif vhost.has_redirect():
+ if not self._has_certbot_redirect_comment(vhost):
+ self._add_redirect_block(vhost, active=False)
+ logger.info("The appropriate server block is already redirecting "
+ "traffic. To enable redirect anyway, uncomment the "
+ "redirect lines in %s.", vhost.filep)
+ else:
+ # Redirect plaintextish host to https
+ self._add_redirect_block(vhost, active=True)
+ logger.info("Redirecting all traffic on port %s to ssl in %s",
+ self.DEFAULT_LISTEN_PORT, vhost.filep)
def _enable_ocsp_stapling(self, domain, chain_path):
"""Include OCSP response in TLS handshake
@@ -671,14 +708,16 @@ class NginxConfigurator(common.Plugin):
"""
save_files = set(self.parser.parsed.keys())
- try:
+ try: # TODO: make a common base for Apache and Nginx plugins
# Create Checkpoint
if temporary:
self.reverter.add_to_temp_checkpoint(
save_files, self.save_notes)
+ # how many comments does it take
else:
self.reverter.add_to_checkpoint(save_files,
self.save_notes)
+ # to confuse a linter?
except errors.ReverterError as err:
raise errors.PluginError(str(err))
diff --git a/certbot-nginx/certbot_nginx/nginxparser.py b/certbot-nginx/certbot_nginx/nginxparser.py
index 6f2a3ec70..2cbd0b86a 100644
--- a/certbot-nginx/certbot_nginx/nginxparser.py
+++ b/certbot-nginx/certbot_nginx/nginxparser.py
@@ -43,7 +43,7 @@ class RawNginxParser(object):
modifier = Literal("=") | Literal("~*") | Literal("~") | Literal("^~")
# rules
- comment = space + Literal('#') + restOfLine()
+ comment = space + Literal('#') + restOfLine
assignment = space + key + Optional(space + value, default=None) + semicolon
location_statement = space + Optional(modifier) + Optional(space + location + space)
diff --git a/certbot-nginx/certbot_nginx/obj.py b/certbot-nginx/certbot_nginx/obj.py
index c58a82450..29fa976f3 100644
--- a/certbot-nginx/certbot_nginx/obj.py
+++ b/certbot-nginx/certbot_nginx/obj.py
@@ -3,6 +3,7 @@ import re
from certbot.plugins import common
+REDIRECT_DIRECTIVES = ['return', 'rewrite']
class Addr(common.Addr):
r"""Represents an Nginx address, i.e. what comes after the 'listen'
@@ -28,10 +29,14 @@ class Addr(common.Addr):
:param bool default: Whether the directive includes 'default_server'
"""
+ UNSPECIFIED_IPV4_ADDRESSES = ('', '*', '0.0.0.0')
+ CANONICAL_UNSPECIFIED_ADDRESS = UNSPECIFIED_IPV4_ADDRESSES[0]
+
def __init__(self, host, port, ssl, default):
super(Addr, self).__init__((host, port))
self.ssl = ssl
self.default = default
+ self.unspecified_address = host in self.UNSPECIFIED_IPV4_ADDRESSES
@classmethod
def fromstring(cls, str_addr):
@@ -92,9 +97,23 @@ class Addr(common.Addr):
def __repr__(self):
return "Addr(" + self.__str__() + ")"
+ def super_eq(self, other):
+ """Check ip/port equality, with IPv6 support.
+ """
+ # If both addresses got an unspecified address, then make sure the
+ # host representation in each match when doing the comparison.
+ if self.unspecified_address and other.unspecified_address:
+ return common.Addr((self.CANONICAL_UNSPECIFIED_ADDRESS,
+ self.tup[1]), self.ipv6) == \
+ common.Addr((other.CANONICAL_UNSPECIFIED_ADDRESS,
+ other.tup[1]), other.ipv6)
+ # Nginx plugin currently doesn't support IPv6 but this will
+ # future-proof it
+ return super(Addr, self).__eq__(other)
+
def __eq__(self, other):
if isinstance(other, self.__class__):
- return (self.tup == other.tup and
+ return (self.super_eq(other) and
self.ssl == other.ssl and
self.default == other.default)
return False
@@ -149,3 +168,32 @@ class VirtualHost(object): # pylint: disable=too-few-public-methods
self.path == other.path)
return False
+
+ def has_redirect(self):
+ """Determine if this vhost has a redirecting statement
+ """
+ for directive_name in REDIRECT_DIRECTIVES:
+ found = _find_directive(self.raw, directive_name)
+ if found is not None:
+ return True
+ return False
+
+ def contains_list(self, test):
+ """Determine if raw server block contains test list at top level
+ """
+ for i in xrange(0, len(self.raw) - len(test)):
+ if self.raw[i:i + len(test)] == test:
+ return True
+ return False
+
+def _find_directive(directives, directive_name):
+ """Find a directive of type directive_name in directives
+ """
+ if not directives or isinstance(directives, str) or len(directives) == 0:
+ return None
+
+ if directives[0] == directive_name:
+ return directives
+
+ matches = (_find_directive(line, directive_name) for line in directives)
+ return next((m for m in matches if m is not None), None)
diff --git a/certbot-nginx/certbot_nginx/parser.py b/certbot-nginx/certbot_nginx/parser.py
index 6203b5f71..1a2c85c2c 100644
--- a/certbot-nginx/certbot_nginx/parser.py
+++ b/certbot-nginx/certbot_nginx/parser.py
@@ -82,21 +82,28 @@ class NginxParser(object):
else:
return path
- def get_vhosts(self):
- # pylint: disable=cell-var-from-loop
- """Gets list of all 'virtual hosts' found in Nginx configuration.
- Technically this is a misnomer because Nginx does not have virtual
- hosts, it has 'server blocks'.
-
- :returns: List of :class:`~certbot_nginx.obj.VirtualHost`
- objects found in configuration
- :rtype: list
-
+ def _build_addr_to_ssl(self):
+ """Builds a map from address to whether it listens on ssl in any server block
"""
- enabled = True # We only look at enabled vhosts for now
- vhosts = []
- servers = {}
+ servers = self._get_raw_servers()
+ addr_to_ssl = {}
+ for filename in servers:
+ for server, _ in servers[filename]:
+ # Parse the server block to save addr info
+ parsed_server = _parse_server_raw(server)
+ for addr in parsed_server['addrs']:
+ addr_tuple = addr.normalized_tuple()
+ if addr_tuple not in addr_to_ssl:
+ addr_to_ssl[addr_tuple] = addr.ssl
+ addr_to_ssl[addr_tuple] = addr.ssl or addr_to_ssl[addr_tuple]
+ return addr_to_ssl
+
+ def _get_raw_servers(self):
+ # pylint: disable=cell-var-from-loop
+ """Get a map of unparsed all server blocks
+ """
+ servers = {}
for filename in self.parsed:
tree = self.parsed[filename]
servers[filename] = []
@@ -110,12 +117,28 @@ class NginxParser(object):
for i, (server, path) in enumerate(servers[filename]):
new_server = self._get_included_directives(server)
servers[filename][i] = (new_server, path)
+ return servers
+ def get_vhosts(self):
+ # pylint: disable=cell-var-from-loop
+ """Gets list of all 'virtual hosts' found in Nginx configuration.
+ Technically this is a misnomer because Nginx does not have virtual
+ hosts, it has 'server blocks'.
+
+ :returns: List of :class:`~certbot_nginx.obj.VirtualHost`
+ objects found in configuration
+ :rtype: list
+
+ """
+ enabled = True # We only look at enabled vhosts for now
+ servers = self._get_raw_servers()
+
+ vhosts = []
for filename in servers:
for server, path in servers[filename]:
# Parse the server block into a VirtualHost object
- parsed_server = parse_server(server)
+ parsed_server = _parse_server_raw(server)
vhost = obj.VirtualHost(filename,
parsed_server['addrs'],
parsed_server['ssl'],
@@ -125,8 +148,20 @@ class NginxParser(object):
path)
vhosts.append(vhost)
+ self._update_vhosts_addrs_ssl(vhosts)
+
return vhosts
+ def _update_vhosts_addrs_ssl(self, vhosts):
+ """Update a list of raw parsed vhosts to include global address sslishness
+ """
+ addr_to_ssl = self._build_addr_to_ssl()
+ for vhost in vhosts:
+ for addr in vhost.addrs:
+ addr.ssl = addr_to_ssl[addr.normalized_tuple()]
+ if addr.ssl:
+ vhost.ssl = True
+
def _get_included_directives(self, block):
"""Returns array with the "include" directives expanded out by
concatenating the contents of the included file to the block.
@@ -241,6 +276,17 @@ class NginxParser(object):
except IOError:
logger.error("Could not open file for writing: %s", filename)
+ def parse_server(self, server):
+ """Parses a list of server directives, accounting for global address sslishness.
+
+ :param list server: list of directives in a server block
+ :rtype: dict
+ """
+ addr_to_ssl = self._build_addr_to_ssl()
+ parsed_server = _parse_server_raw(server)
+ _apply_global_addr_ssl(addr_to_ssl, parsed_server)
+ return parsed_server
+
def has_ssl_on_directive(self, vhost):
"""Does vhost have ssl on for all ports?
@@ -290,7 +336,7 @@ class NginxParser(object):
# update vhost based on new directives
new_server = self._get_included_directives(result)
- parsed_server = parse_server(new_server)
+ parsed_server = self.parse_server(new_server)
vhost.addrs = parsed_server['addrs']
vhost.ssl = parsed_server['ssl']
vhost.names = parsed_server['names']
@@ -298,33 +344,6 @@ class NginxParser(object):
except errors.MisconfigurationError as err:
raise errors.MisconfigurationError("Problem in %s: %s" % (filename, err.message))
- def get_all_certs_keys(self):
- """Gets all certs and keys in the nginx config.
-
- :returns: list of tuples with form [(cert, key, path)]
- cert - str path to certificate file
- key - str path to associated key file
- path - File path to configuration file.
- :rtype: set
-
- """
- c_k = set()
- vhosts = self.get_vhosts()
- for vhost in vhosts:
- tup = [None, None, vhost.filep]
- if vhost.ssl:
- for directive in vhost.raw:
- # A directive can be an empty list to preserve whitespace
- if not directive:
- continue
- if directive[0] == 'ssl_certificate':
- tup[0] = directive[1]
- elif directive[0] == 'ssl_certificate_key':
- tup[1] = directive[1]
- if tup[0] is not None and tup[1] is not None:
- c_k.add(tuple(tup))
- return c_k
-
def _do_for_subarray(entry, condition, func, path=None):
"""Executes a function for a subarray of a nested array if it matches
@@ -461,42 +480,6 @@ def _get_servernames(names):
names = re.sub(whitespace_re, ' ', names)
return names.split(' ')
-
-def parse_server(server):
- """Parses a list of server directives.
-
- :param list server: list of directives in a server block
- :rtype: dict
-
- """
- parsed_server = {'addrs': set(),
- 'ssl': False,
- 'names': set()}
-
- apply_ssl_to_all_addrs = False
-
- for directive in server:
- if not directive:
- continue
- if directive[0] == 'listen':
- addr = obj.Addr.fromstring(directive[1])
- parsed_server['addrs'].add(addr)
- if not parsed_server['ssl'] and addr.ssl:
- parsed_server['ssl'] = True
- elif directive[0] == 'server_name':
- parsed_server['names'].update(
- _get_servernames(directive[1]))
- elif directive[0] == 'ssl' and directive[1] == 'on':
- parsed_server['ssl'] = True
- apply_ssl_to_all_addrs = True
-
- if apply_ssl_to_all_addrs:
- for addr in parsed_server['addrs']:
- addr.ssl = True
-
- return parsed_server
-
-
def _add_directives(block, directives, replace):
"""Adds or replaces directives in a config block.
@@ -577,3 +560,44 @@ def _add_directive(block, directive, replace):
'tried to insert directive "{0}" but found '
'conflicting "{1}".'.format(directive, block[location]))
+def _apply_global_addr_ssl(addr_to_ssl, parsed_server):
+ """Apply global sslishness information to the parsed server block
+ """
+ for addr in parsed_server['addrs']:
+ addr.ssl = addr_to_ssl[addr.normalized_tuple()]
+ if addr.ssl:
+ parsed_server['ssl'] = True
+
+def _parse_server_raw(server):
+ """Parses a list of server directives.
+
+ :param list server: list of directives in a server block
+ :rtype: dict
+
+ """
+ parsed_server = {'addrs': set(),
+ 'ssl': False,
+ 'names': set()}
+
+ apply_ssl_to_all_addrs = False
+
+ for directive in server:
+ if not directive:
+ continue
+ if directive[0] == 'listen':
+ addr = obj.Addr.fromstring(directive[1])
+ parsed_server['addrs'].add(addr)
+ if addr.ssl:
+ parsed_server['ssl'] = True
+ elif directive[0] == 'server_name':
+ parsed_server['names'].update(
+ _get_servernames(directive[1]))
+ elif directive[0] == 'ssl' and directive[1] == 'on':
+ parsed_server['ssl'] = True
+ apply_ssl_to_all_addrs = True
+
+ if apply_ssl_to_all_addrs:
+ for addr in parsed_server['addrs']:
+ addr.ssl = True
+
+ return parsed_server
diff --git a/certbot-nginx/certbot_nginx/tests/configurator_test.py b/certbot-nginx/certbot_nginx/tests/configurator_test.py
index d871a5720..cc36aa0de 100644
--- a/certbot-nginx/certbot_nginx/tests/configurator_test.py
+++ b/certbot-nginx/certbot_nginx/tests/configurator_test.py
@@ -21,6 +21,8 @@ from certbot_nginx.tests import util
class NginxConfiguratorTest(util.NginxTest):
"""Test a semi complex vhost configuration."""
+ _multiprocess_can_split_ = True
+
def setUp(self):
super(NginxConfiguratorTest, self).setUp()
@@ -40,7 +42,7 @@ class NginxConfiguratorTest(util.NginxTest):
def test_prepare(self):
self.assertEqual((1, 6, 2), self.config.version)
- self.assertEqual(7, len(self.config.parser.parsed))
+ self.assertEqual(8, len(self.config.parser.parsed))
# ensure we successfully parsed a file for ssl_options
self.assertTrue(self.config.parser.loc["ssl_options"])
@@ -68,7 +70,8 @@ class NginxConfiguratorTest(util.NginxTest):
names = self.config.get_all_names()
self.assertEqual(names, set(
["155.225.50.69.nephoscale.net", "www.example.org", "another.alias",
- "migration.com", "summer.com", "geese.com", "sslon.com"]))
+ "migration.com", "summer.com", "geese.com", "sslon.com",
+ "globalssl.com", "globalsslsetssl.com"]))
def test_supported_enhancements(self):
self.assertEqual(['redirect', 'staple-ocsp'],
@@ -155,6 +158,18 @@ class NginxConfiguratorTest(util.NginxTest):
"example/chain.pem",
None)
+ @mock.patch('certbot_nginx.parser.NginxParser.add_server_directives')
+ def test_deploy_cert_raise_on_add_error(self, mock_add_server_directives):
+ mock_add_server_directives.side_effect = errors.MisconfigurationError()
+ self.assertRaises(
+ errors.PluginError,
+ self.config.deploy_cert,
+ "migration.com",
+ "example/cert.pem",
+ "example/key.pem",
+ "example/chain.pem",
+ "example/fullchain.pem")
+
def test_deploy_cert(self):
server_conf = self.config.parser.abs_path('server.conf')
nginx_conf = self.config.parser.abs_path('nginx.conf')
@@ -238,41 +253,6 @@ class NginxConfiguratorTest(util.NginxTest):
],
parsed_migration_conf[0])
- def test_get_all_certs_keys(self):
- nginx_conf = self.config.parser.abs_path('nginx.conf')
- example_conf = self.config.parser.abs_path('sites-enabled/example.com')
- migration_conf = self.config.parser.abs_path('sites-enabled/migration.com')
- sslon_conf = self.config.parser.abs_path('sites-enabled/sslon.com')
-
- # Get the default SSL vhost
- self.config.deploy_cert(
- "www.example.com",
- "example/cert.pem",
- "example/key.pem",
- "example/chain.pem",
- "example/fullchain.pem")
- self.config.deploy_cert(
- "another.alias",
- "/etc/nginx/cert.pem",
- "/etc/nginx/key.pem",
- "/etc/nginx/chain.pem",
- "/etc/nginx/fullchain.pem")
- self.config.deploy_cert(
- "migration.com",
- "migration/cert.pem",
- "migration/key.pem",
- "migration/chain.pem",
- "migration/fullchain.pem")
- self.config.save()
-
- self.config.parser.load()
- self.assertEqual(set([
- ('example/fullchain.pem', 'example/key.pem', example_conf),
- ('/etc/nginx/fullchain.pem', '/etc/nginx/key.pem', nginx_conf),
- ('migration/fullchain.pem', 'migration/key.pem', migration_conf),
- ('snakeoil.cert', 'snakeoil.key', sslon_conf),
- ]), self.config.get_all_certs_keys())
-
@mock.patch("certbot_nginx.configurator.tls_sni_01.NginxTlsSni01.perform")
@mock.patch("certbot_nginx.configurator.NginxConfigurator.restart")
@mock.patch("certbot_nginx.configurator.NginxConfigurator.revert_challenge_config")
@@ -377,15 +357,15 @@ class NginxConfiguratorTest(util.NginxTest):
mock_popen.side_effect = OSError("Can't find program")
self.assertRaises(errors.MisconfigurationError, self.config.restart)
- @mock.patch("certbot.util.run_script")
- def test_config_test(self, _):
- self.config.config_test()
-
@mock.patch("certbot.util.run_script")
def test_config_test_bad_process(self, mock_run_script):
mock_run_script.side_effect = errors.SubprocessError
self.assertRaises(errors.MisconfigurationError, self.config.config_test)
+ @mock.patch("certbot.util.run_script")
+ def test_config_test(self, _):
+ self.config.config_test()
+
@mock.patch("certbot.reverter.Reverter.recovery_routine")
def test_recovery_routine_throws_error_from_reverter(self, mock_recovery_routine):
mock_recovery_routine.side_effect = errors.ReverterError("foo")
@@ -445,6 +425,64 @@ class NginxConfiguratorTest(util.NginxTest):
generated_conf = self.config.parser.parsed[migration_conf]
self.assertTrue(util.contains_at_depth(generated_conf, expected, 2))
+ @mock.patch('certbot_nginx.obj.VirtualHost.contains_list')
+ @mock.patch('certbot_nginx.obj.VirtualHost.has_redirect')
+ def test_certbot_redirect_exists(self, mock_has_redirect, mock_contains_list):
+ # Test that we add no redirect statement if there is already a
+ # redirect in the block that is managed by certbot
+ # Has a certbot redirect
+ mock_has_redirect.return_value = True
+ mock_contains_list.return_value = True
+ with mock.patch("certbot_nginx.configurator.logger") as mock_logger:
+ self.config.enhance("www.example.com", "redirect")
+ self.assertEqual(mock_logger.info.call_args[0][0],
+ "Traffic on port %s already redirecting to ssl in %s")
+
+ @mock.patch('certbot_nginx.obj.VirtualHost.contains_list')
+ @mock.patch('certbot_nginx.obj.VirtualHost.has_redirect')
+ def test_non_certbot_redirect_exists(self, mock_has_redirect, mock_contains_list):
+ # Test that we add a redirect as a comment if there is already a
+ # redirect-class statement in the block that isn't managed by certbot
+ example_conf = self.config.parser.abs_path('sites-enabled/example.com')
+
+ # Has a non-Certbot redirect, and has no existing comment
+ mock_contains_list.return_value = False
+ mock_has_redirect.return_value = True
+ with mock.patch("certbot_nginx.configurator.logger") as mock_logger:
+ self.config.enhance("www.example.com", "redirect")
+ self.assertEqual(mock_logger.info.call_args[0][0],
+ "The appropriate server block is already redirecting "
+ "traffic. To enable redirect anyway, uncomment the "
+ "redirect lines in %s.")
+ generated_conf = self.config.parser.parsed[example_conf]
+ expected = [
+ ['#', ' Redirect non-https traffic to https'],
+ ['#', ' if ($scheme != "https") {'],
+ ['#', ' return 301 https://$host$request_uri;'],
+ ['#', ' } # managed by Certbot']
+ ]
+ for line in expected:
+ self.assertTrue(util.contains_at_depth(generated_conf, line, 2))
+
+ @mock.patch('certbot_nginx.obj.VirtualHost.contains_list')
+ @mock.patch('certbot_nginx.obj.VirtualHost.has_redirect')
+ @mock.patch('certbot_nginx.configurator.NginxConfigurator._has_certbot_redirect_comment')
+ @mock.patch('certbot_nginx.configurator.NginxConfigurator._add_redirect_block')
+ def test_redirect_comment_exists(self, mock_add_redirect_block,
+ mock_has_cb_redirect_comment, mock_has_redirect, mock_contains_list):
+ # Test that we add nothing if there is a non-Certbot redirect and a
+ # preexisting comment
+ # Has a non-Certbot redirect and a comment
+ mock_has_redirect.return_value = True
+ mock_contains_list.return_value = False # self._has_certbot_redirect(vhost):
+ mock_has_cb_redirect_comment.return_value = True
+
+ # assert _add_redirect_block not called
+ with mock.patch("certbot_nginx.configurator.logger") as mock_logger:
+ self.config.enhance("www.example.com", "redirect")
+ self.assertFalse(mock_add_redirect_block.called)
+ self.assertTrue(mock_logger.info.called)
+
def test_redirect_dont_enhance(self):
# Test that we don't accidentally add redirect to ssl-only block
with mock.patch("certbot_nginx.configurator.logger") as mock_logger:
diff --git a/certbot-nginx/certbot_nginx/tests/nginxparser_test.py b/certbot-nginx/certbot_nginx/tests/nginxparser_test.py
index 5c8d6d215..e83b414cf 100644
--- a/certbot-nginx/certbot_nginx/tests/nginxparser_test.py
+++ b/certbot-nginx/certbot_nginx/tests/nginxparser_test.py
@@ -1,7 +1,7 @@
"""Test for certbot_nginx.nginxparser."""
import copy
import operator
-import os
+import tempfile
import unittest
from pyparsing import ParseException
@@ -128,44 +128,34 @@ class TestRawNginxParser(unittest.TestCase):
[['root', ' ', 'html'],
['index', ' ', 'index.html index.htm']]]]]))
- with open(util.get_data_filename('nginx.new.conf'), 'w') as handle:
- dump(parsed, handle)
- with open(util.get_data_filename('nginx.new.conf')) as handle:
- parsed_new = load(handle)
- try:
- self.maxDiff = None
- self.assertEqual(parsed[0], parsed_new[0])
- self.assertEqual(parsed[1:], parsed_new[1:])
- finally:
- os.unlink(util.get_data_filename('nginx.new.conf'))
+ with tempfile.TemporaryFile() as f:
+ dump(parsed, f)
+ f.seek(0)
+ parsed_new = load(f)
+ self.assertEqual(parsed, parsed_new)
def test_comments(self):
with open(util.get_data_filename('minimalistic_comments.conf')) as handle:
parsed = load(handle)
- with open(util.get_data_filename('minimalistic_comments.new.conf'), 'w') as handle:
- dump(parsed, handle)
+ with tempfile.TemporaryFile() as f:
+ dump(parsed, f)
+ f.seek(0)
+ parsed_new = load(f)
- with open(util.get_data_filename('minimalistic_comments.new.conf')) as handle:
- parsed_new = load(handle)
-
- try:
- self.assertEqual(parsed, parsed_new)
-
- self.assertEqual(parsed_new, [
- ['#', " Use bar.conf when it's a full moon!"],
- ['include', 'foo.conf'],
- ['#', ' Kilroy was here'],
- ['check_status'],
- [['server'],
- [['#', ''],
- ['#', " Don't forget to open up your firewall!"],
- ['#', ''],
- ['listen', '1234'],
- ['#', ' listen 80;']]],
- ])
- finally:
- os.unlink(util.get_data_filename('minimalistic_comments.new.conf'))
+ self.assertEqual(parsed, parsed_new)
+ self.assertEqual(parsed_new, [
+ ['#', " Use bar.conf when it's a full moon!"],
+ ['include', 'foo.conf'],
+ ['#', ' Kilroy was here'],
+ ['check_status'],
+ [['server'],
+ [['#', ''],
+ ['#', " Don't forget to open up your firewall!"],
+ ['#', ''],
+ ['listen', '1234'],
+ ['#', ' listen 80;']]],
+ ])
def test_issue_518(self):
parsed = loads('if ($http_accept ~* "webp") { set $webp "true"; }')
diff --git a/certbot-nginx/certbot_nginx/tests/obj_test.py b/certbot-nginx/certbot_nginx/tests/obj_test.py
index 84d0c6bca..b0a2d5ad8 100644
--- a/certbot-nginx/certbot_nginx/tests/obj_test.py
+++ b/certbot-nginx/certbot_nginx/tests/obj_test.py
@@ -1,5 +1,6 @@
"""Test the helper objects in certbot_nginx.obj."""
import unittest
+import itertools
class AddrTest(unittest.TestCase):
@@ -72,6 +73,24 @@ class AddrTest(unittest.TestCase):
self.assertNotEqual(self.addr1, self.addr2)
self.assertFalse(self.addr1 == 3333)
+ def test_equivalent_any_addresses(self):
+ from certbot_nginx.obj import Addr
+ any_addresses = ("0.0.0.0:80 default_server ssl",
+ "80 default_server ssl",
+ "*:80 default_server ssl")
+ for first, second in itertools.combinations(any_addresses, 2):
+ self.assertEqual(Addr.fromstring(first), Addr.fromstring(second))
+
+ # Also, make sure ports are checked.
+ self.assertNotEqual(Addr.fromstring(any_addresses[0]),
+ Addr.fromstring("0.0.0.0:443 default_server ssl"))
+
+ # And they aren't equivalent to a specified address.
+ for any_address in any_addresses:
+ self.assertNotEqual(
+ Addr.fromstring("192.168.1.2:80 default_server ssl"),
+ Addr.fromstring(any_address))
+
def test_set_inclusion(self):
from certbot_nginx.obj import Addr
set_a = set([self.addr1, self.addr2])
@@ -87,10 +106,43 @@ class VirtualHostTest(unittest.TestCase):
def setUp(self):
from certbot_nginx.obj import VirtualHost
from certbot_nginx.obj import Addr
+ raw1 = [
+ ['listen', '69.50.225.155:9000'],
+ [['if', '($scheme != "https") '],
+ [['return', '301 https://$host$request_uri']]
+ ],
+ ['#', ' managed by Certbot']
+ ]
self.vhost1 = VirtualHost(
"filep",
set([Addr.fromstring("localhost")]), False, False,
- set(['localhost']), [], [])
+ set(['localhost']), raw1, [])
+ raw2 = [
+ ['listen', '69.50.225.155:9000'],
+ [['if', '($scheme != "https") '],
+ [['return', '301 https://$host$request_uri']]
+ ]
+ ]
+ self.vhost2 = VirtualHost(
+ "filep",
+ set([Addr.fromstring("localhost")]), False, False,
+ set(['localhost']), raw2, [])
+ raw3 = [
+ ['listen', '69.50.225.155:9000'],
+ ['rewrite', '^(.*)$ $scheme://www.domain.com$1 permanent;']
+ ]
+ self.vhost3 = VirtualHost(
+ "filep",
+ set([Addr.fromstring("localhost")]), False, False,
+ set(['localhost']), raw3, [])
+ raw4 = [
+ ['listen', '69.50.225.155:9000'],
+ ['server_name', 'return.com']
+ ]
+ self.vhost4 = VirtualHost(
+ "filp",
+ set([Addr.fromstring("localhost")]), False, False,
+ set(['localhost']), raw4, [])
def test_eq(self):
from certbot_nginx.obj import Addr
@@ -110,6 +162,47 @@ class VirtualHostTest(unittest.TestCase):
'enabled: False'])
self.assertEqual(stringified, str(self.vhost1))
+ def test_has_redirect(self):
+ self.assertTrue(self.vhost1.has_redirect())
+ self.assertTrue(self.vhost2.has_redirect())
+ self.assertTrue(self.vhost3.has_redirect())
+ self.assertFalse(self.vhost4.has_redirect())
+
+ def test_contains_list(self):
+ from certbot_nginx.obj import VirtualHost
+ from certbot_nginx.obj import Addr
+ from certbot_nginx.configurator import TEST_REDIRECT_BLOCK
+ test_needle = TEST_REDIRECT_BLOCK
+ test_haystack = [['listen', '80'], ['root', '/var/www/html'],
+ ['index', 'index.html index.htm index.nginx-debian.html'],
+ ['server_name', 'two.functorkitten.xyz'], ['listen', '443 ssl'],
+ ['#', ' managed by Certbot'],
+ ['ssl_certificate', '/etc/letsencrypt/live/two.functorkitten.xyz/fullchain.pem'],
+ ['#', ' managed by Certbot'],
+ ['ssl_certificate_key', '/etc/letsencrypt/live/two.functorkitten.xyz/privkey.pem'],
+ ['#', ' managed by Certbot'],
+ [['if', '($scheme != "https")'], [['return', '301 https://$host$request_uri']]],
+ ['#', ' managed by Certbot'], []]
+ vhost_haystack = VirtualHost(
+ "filp",
+ set([Addr.fromstring("localhost")]), False, False,
+ set(['localhost']), test_haystack, [])
+ test_bad_haystack = [['listen', '80'], ['root', '/var/www/html'],
+ ['index', 'index.html index.htm index.nginx-debian.html'],
+ ['server_name', 'two.functorkitten.xyz'], ['listen', '443 ssl'],
+ ['#', ' managed by Certbot'],
+ ['ssl_certificate', '/etc/letsencrypt/live/two.functorkitten.xyz/fullchain.pem'],
+ ['#', ' managed by Certbot'],
+ ['ssl_certificate_key', '/etc/letsencrypt/live/two.functorkitten.xyz/privkey.pem'],
+ ['#', ' managed by Certbot'],
+ [['if', '($scheme != "https")'], [['return', '302 https://$host$request_uri']]],
+ ['#', ' managed by Certbot'], []]
+ vhost_bad_haystack = VirtualHost(
+ "filp",
+ set([Addr.fromstring("localhost")]), False, False,
+ set(['localhost']), test_bad_haystack, [])
+ self.assertTrue(vhost_haystack.contains_list(test_needle))
+ self.assertFalse(vhost_bad_haystack.contains_list(test_needle))
if __name__ == "__main__":
unittest.main() # pragma: no cover
diff --git a/certbot-nginx/certbot_nginx/tests/parser_test.py b/certbot-nginx/certbot_nginx/tests/parser_test.py
index d5593171a..921cc3c5a 100644
--- a/certbot-nginx/certbot_nginx/tests/parser_test.py
+++ b/certbot-nginx/certbot_nginx/tests/parser_test.py
@@ -49,7 +49,8 @@ class NginxParserTest(util.NginxTest):
'sites-enabled/default',
'sites-enabled/example.com',
'sites-enabled/migration.com',
- 'sites-enabled/sslon.com']]),
+ 'sites-enabled/sslon.com',
+ 'sites-enabled/globalssl.com']]),
set(nparser.parsed.keys()))
self.assertEqual([['server_name', 'somename alias another.alias']],
nparser.parsed[nparser.abs_path('server.conf')])
@@ -73,7 +74,7 @@ class NginxParserTest(util.NginxTest):
parsed = nparser._parse_files(nparser.abs_path(
'sites-enabled/example.com.test'))
self.assertEqual(3, len(glob.glob(nparser.abs_path('*.test'))))
- self.assertEqual(4, len(
+ self.assertEqual(5, len(
glob.glob(nparser.abs_path('sites-enabled/*.test'))))
self.assertEqual([[['server'], [['listen', '69.50.225.155:9000'],
['listen', '127.0.0.1'],
@@ -104,6 +105,16 @@ class NginxParserTest(util.NginxTest):
lambda x, y, pts=paths: pts.append(y))
self.assertEqual(paths, result)
+ def test_get_vhosts_global_ssl(self):
+ nparser = parser.NginxParser(self.config_path, self.ssl_options)
+ vhosts = nparser.get_vhosts()
+
+ vhost = obj.VirtualHost(nparser.abs_path('sites-enabled/globalssl.com'),
+ [obj.Addr('4.8.2.6', '57', True, False)],
+ True, True, set(['globalssl.com']), [], [0])
+
+ globalssl_com = [x for x in vhosts if 'globalssl.com' in x.filep][0]
+ self.assertEqual(vhost, globalssl_com)
def test_get_vhosts(self):
nparser = parser.NginxParser(self.config_path, self.ssl_options)
@@ -137,7 +148,7 @@ class NginxParserTest(util.NginxTest):
'*.www.example.com']),
[], [2, 1, 0])
- self.assertEqual(8, len(vhosts))
+ self.assertEqual(10, len(vhosts))
example_com = [x for x in vhosts if 'example.com' in x.filep][0]
self.assertEqual(vhost3, example_com)
default = [x for x in vhosts if 'default' in x.filep][0]
@@ -291,47 +302,34 @@ class NginxParserTest(util.NginxTest):
COMMENT_BLOCK,
["\n", "e", " ", "f"]])
- def test_get_all_certs_keys(self):
- nparser = parser.NginxParser(self.config_path, self.ssl_options)
- filep = nparser.abs_path('sites-enabled/example.com')
- mock_vhost = obj.VirtualHost(filep,
- None, None, None,
- set(['.example.com', 'example.*']),
- None, [0])
- nparser.add_server_directives(mock_vhost,
- [['ssl_certificate', 'foo.pem'],
- ['ssl_certificate_key', 'bar.key'],
- ['listen', '443 ssl']],
- replace=False)
- c_k = nparser.get_all_certs_keys()
- migration_file = nparser.abs_path('sites-enabled/migration.com')
- sslon_file = nparser.abs_path('sites-enabled/sslon.com')
- self.assertEqual(set([('foo.pem', 'bar.key', filep),
- ('cert.pem', 'cert.key', migration_file),
- ('snakeoil.cert', 'snakeoil.key', sslon_file)
- ]), c_k)
-
- def test_parse_server_ssl(self):
- server = parser.parse_server([
+ def test_parse_server_raw_ssl(self):
+ server = parser._parse_server_raw([ #pylint: disable=protected-access
['listen', '443']
])
self.assertFalse(server['ssl'])
- server = parser.parse_server([
+ server = parser._parse_server_raw([ #pylint: disable=protected-access
['listen', '443 ssl']
])
self.assertTrue(server['ssl'])
- server = parser.parse_server([
+ server = parser._parse_server_raw([ #pylint: disable=protected-access
['listen', '443'], ['ssl', 'off']
])
self.assertFalse(server['ssl'])
- server = parser.parse_server([
+ server = parser._parse_server_raw([ #pylint: disable=protected-access
['listen', '443'], ['ssl', 'on']
])
self.assertTrue(server['ssl'])
+ def test_parse_server_global_ssl_applied(self):
+ nparser = parser.NginxParser(self.config_path, self.ssl_options)
+ server = nparser.parse_server([
+ ['listen', '443']
+ ])
+ self.assertTrue(server['ssl'])
+
def test_ssl_options_should_be_parsed_ssl_directives(self):
nparser = parser.NginxParser(self.config_path, self.ssl_options)
self.assertEqual(nginxparser.UnspacedList(nparser.loc["ssl_options"]),
diff --git a/certbot-nginx/certbot_nginx/tests/testdata/etc_nginx/sites-enabled/globalssl.com b/certbot-nginx/certbot_nginx/tests/testdata/etc_nginx/sites-enabled/globalssl.com
new file mode 100644
index 000000000..969447d6e
--- /dev/null
+++ b/certbot-nginx/certbot_nginx/tests/testdata/etc_nginx/sites-enabled/globalssl.com
@@ -0,0 +1,9 @@
+server {
+ server_name globalssl.com;
+ listen 4.8.2.6:57;
+}
+
+server {
+ server_name globalsslsetssl.com;
+ listen 4.8.2.6:57 ssl;
+}
diff --git a/certbot-nginx/certbot_nginx/tests/util.py b/certbot-nginx/certbot_nginx/tests/util.py
index 96fdac527..2fb866b77 100644
--- a/certbot-nginx/certbot_nginx/tests/util.py
+++ b/certbot-nginx/certbot_nginx/tests/util.py
@@ -11,7 +11,7 @@ from acme import jose
from certbot import configuration
-from certbot.tests import test_util
+from certbot.tests import util as test_util
from certbot.plugins import common
diff --git a/certbot-nginx/setup.py b/certbot-nginx/setup.py
index 4c39d37c2..e140c75d2 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.10.0.dev0'
+version = '0.11.0.dev0'
# Please update tox.ini when modifying dependency version requirements
install_requires = [
diff --git a/certbot/__init__.py b/certbot/__init__.py
index 45892e269..376b0504f 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.10.0.dev0'
+__version__ = '0.11.0.dev0'
diff --git a/certbot/cert_manager.py b/certbot/cert_manager.py
new file mode 100644
index 000000000..ae4c5a722
--- /dev/null
+++ b/certbot/cert_manager.py
@@ -0,0 +1,253 @@
+"""Tools for managing certificates."""
+import datetime
+import logging
+import os
+import pytz
+import traceback
+import zope.component
+
+from certbot import errors
+from certbot import interfaces
+from certbot import ocsp
+from certbot import storage
+from certbot import util
+
+from certbot.display import util as display_util
+
+logger = logging.getLogger(__name__)
+
+###################
+# Commands
+###################
+
+def update_live_symlinks(config):
+ """Update the certificate file family symlinks to use archive_dir.
+
+ Use the information in the config file to make symlinks point to
+ the correct archive directory.
+
+ .. note:: This assumes that the installation is using a Reverter object.
+
+ :param config: Configuration.
+ :type config: :class:`certbot.configuration.NamespaceConfig`
+
+ """
+ for renewal_file in storage.renewal_conf_files(config):
+ storage.RenewableCert(renewal_file, config, update_symlinks=True)
+
+def rename_lineage(config):
+ """Rename the specified lineage to the new name.
+
+ :param config: Configuration.
+ :type config: :class:`certbot.configuration.NamespaceConfig`
+
+ """
+ disp = zope.component.getUtility(interfaces.IDisplay)
+
+ certname = _get_certname(config, "rename")
+
+ new_certname = config.new_certname
+ if not new_certname:
+ code, new_certname = disp.input(
+ "Enter the new name for certificate {0}".format(certname),
+ flag="--updated-cert-name", force_interactive=True)
+ if code != display_util.OK or not new_certname:
+ raise errors.Error("User ended interaction.")
+
+ lineage = lineage_for_certname(config, certname)
+ if not lineage:
+ raise errors.ConfigurationError("No existing certificate with name "
+ "{0} found.".format(certname))
+ storage.rename_renewal_config(certname, new_certname, config)
+ disp.notification("Successfully renamed {0} to {1}."
+ .format(certname, new_certname), pause=False)
+
+def certificates(config):
+ """Display information about certs configured with Certbot
+
+ :param config: Configuration.
+ :type config: :class:`certbot.configuration.NamespaceConfig`
+ """
+ parsed_certs = []
+ parse_failures = []
+ for renewal_file in storage.renewal_conf_files(config):
+ try:
+ renewal_candidate = storage.RenewableCert(renewal_file, config)
+ parsed_certs.append(renewal_candidate)
+ except Exception as e: # pylint: disable=broad-except
+ logger.warning("Renewal configuration file %s produced an "
+ "unexpected error: %s. Skipping.", renewal_file, e)
+ logger.debug("Traceback was:\n%s", traceback.format_exc())
+ parse_failures.append(renewal_file)
+
+ # Describe all the certs
+ _describe_certs(config, parsed_certs, parse_failures)
+
+def delete(config):
+ """Delete Certbot files associated with a certificate lineage."""
+ certname = _get_certname(config, "delete")
+ storage.delete_files(config, certname)
+ disp = zope.component.getUtility(interfaces.IDisplay)
+ disp.notification("Deleted all files relating to certificate {0}."
+ .format(certname), pause=False)
+
+###################
+# Public Helpers
+###################
+
+def lineage_for_certname(config, certname):
+ """Find a lineage object with name certname."""
+ def update_cert_for_name_match(candidate_lineage, rv):
+ """Return cert if it has name certname, else return rv
+ """
+ matching_lineage_name_cert = rv
+ if candidate_lineage.lineagename == certname:
+ matching_lineage_name_cert = candidate_lineage
+ return matching_lineage_name_cert
+ return _search_lineages(config, update_cert_for_name_match, None)
+
+def domains_for_certname(config, certname):
+ """Find the domains in the cert with name certname."""
+ def update_domains_for_name_match(candidate_lineage, rv):
+ """Return domains if certname matches, else return rv
+ """
+ matching_domains = rv
+ if candidate_lineage.lineagename == certname:
+ matching_domains = candidate_lineage.names()
+ return matching_domains
+ return _search_lineages(config, update_domains_for_name_match, None)
+
+def find_duplicative_certs(config, domains):
+ """Find existing certs that duplicate the request."""
+ def update_certs_for_domain_matches(candidate_lineage, rv):
+ """Return cert as identical_names_cert if it matches,
+ or subset_names_cert if it matches as subset
+ """
+ # TODO: Handle these differently depending on whether they are
+ # expired or still valid?
+ identical_names_cert, subset_names_cert = rv
+ candidate_names = set(candidate_lineage.names())
+ if candidate_names == set(domains):
+ identical_names_cert = candidate_lineage
+ elif candidate_names.issubset(set(domains)):
+ # This logic finds and returns the largest subset-names cert
+ # in the case where there are several available.
+ if subset_names_cert is None:
+ subset_names_cert = candidate_lineage
+ elif len(candidate_names) > len(subset_names_cert.names()):
+ subset_names_cert = candidate_lineage
+ return (identical_names_cert, subset_names_cert)
+
+ return _search_lineages(config, update_certs_for_domain_matches, (None, None))
+
+
+###################
+# Private Helpers
+###################
+
+def _get_certname(config, verb):
+ """Get certname from flag, interactively, or error out.
+ """
+ certname = config.certname
+ if not certname:
+ disp = zope.component.getUtility(interfaces.IDisplay)
+ filenames = storage.renewal_conf_files(config)
+ choices = [storage.lineagename_for_filename(name) for name in filenames]
+ 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",
+ force_interactive=True)
+ if code != display_util.OK or not index in range(0, len(choices)):
+ raise errors.Error("User ended interaction.")
+ certname = choices[index]
+ return certname
+
+def _report_lines(msgs):
+ """Format a results report for a category of single-line renewal outcomes"""
+ return " " + "\n ".join(str(msg) for msg in msgs)
+
+def _report_human_readable(config, parsed_certs):
+ """Format a results report for a parsed cert"""
+ certinfo = []
+ checker = ocsp.RevocationChecker()
+ for cert in parsed_certs:
+ if config.certname and cert.lineagename != config.certname:
+ continue
+ if config.domains and not set(config.domains).issubset(cert.names()):
+ continue
+ now = pytz.UTC.fromutc(datetime.datetime.utcnow())
+
+ reasons = []
+ if cert.is_test_cert:
+ reasons.append('TEST_CERT')
+ if cert.target_expiry <= now:
+ reasons.append('EXPIRED')
+ if checker.ocsp_revoked(cert.cert, cert.chain):
+ reasons.append('REVOKED')
+
+ if reasons:
+ status = "INVALID: " + ", ".join(reasons)
+ else:
+ diff = cert.target_expiry - now
+ if diff.days == 1:
+ status = "VALID: 1 day"
+ elif diff.days < 1:
+ status = "VALID: {0} hour(s)".format(diff.seconds // 3600)
+ else:
+ status = "VALID: {0} days".format(diff.days)
+
+ valid_string = "{0} ({1})".format(cert.target_expiry, status)
+ certinfo.append(" Certificate Name: {0}\n"
+ " Domains: {1}\n"
+ " Expiry Date: {2}\n"
+ " Certificate Path: {3}\n"
+ " Private Key Path: {4}".format(
+ cert.lineagename,
+ " ".join(cert.names()),
+ valid_string,
+ cert.fullchain,
+ cert.privkey))
+ return "\n".join(certinfo)
+
+def _describe_certs(config, parsed_certs, parse_failures):
+ """Print information about the certs we know about"""
+ out = []
+
+ notify = out.append
+
+ if not parsed_certs and not parse_failures:
+ notify("No certs found.")
+ else:
+ if parsed_certs:
+ match = "matching " if config.certname or config.domains else ""
+ notify("Found the following {0}certs:".format(match))
+ notify(_report_human_readable(config, parsed_certs))
+ if parse_failures:
+ notify("\nThe following renewal configuration files "
+ "were invalid:")
+ notify(_report_lines(parse_failures))
+
+ disp = zope.component.getUtility(interfaces.IDisplay)
+ disp.notification("\n".join(out), pause=False, wrap=False)
+
+def _search_lineages(cli_config, func, initial_rv):
+ """Iterate func over unbroken lineages, allowing custom return conditions.
+
+ Allows flexible customization of return values, including multiple
+ return values and complex checks.
+ """
+ configs_dir = cli_config.renewal_configs_dir
+ # Verify the directory is there
+ util.make_or_verify_dir(configs_dir, mode=0o755, uid=os.geteuid())
+
+ rv = initial_rv
+ for renewal_file in storage.renewal_conf_files(cli_config):
+ try:
+ candidate_lineage = storage.RenewableCert(renewal_file, cli_config)
+ except (errors.CertStorageError, IOError):
+ logger.debug("Renewal conf file %s is broken. Skipping.", renewal_file)
+ logger.debug("Traceback was:\n%s", traceback.format_exc())
+ continue
+ rv = func(candidate_lineage, rv)
+ return rv
diff --git a/certbot/cli.py b/certbot/cli.py
index 83697d8da..d51fd58e0 100644
--- a/certbot/cli.py
+++ b/certbot/cli.py
@@ -18,7 +18,6 @@ import certbot
from certbot import constants
from certbot import crypto_util
from certbot import errors
-from certbot import hooks
from certbot import interfaces
from certbot import util
@@ -51,47 +50,55 @@ cli_command = LEAUTO if fragment in sys.argv[0] else "certbot"
# to replace as much of it as we can...
# This is the stub to include in help generated by argparse
-
SHORT_USAGE = """
- {0} [SUBCOMMAND] [options] [-d domain] [-d domain] ...
+ {0} [SUBCOMMAND] [options] [-d DOMAIN] [-d DOMAIN] ...
Certbot can obtain and install HTTPS/TLS/SSL certificates. By default,
it will attempt to use a webserver both for obtaining and installing the
-cert. Major SUBCOMMANDS are:
+cert. """.format(cli_command)
- (default) run Obtain & install a cert in your current webserver
- certonly Obtain cert, but do not install it (aka "auth")
- install Install a previously obtained cert in a server
- renew Renew previously obtained certs that are near expiry
- revoke Revoke a previously obtained certificate
- register Perform tasks related to registering with the CA
- rollback Rollback server configuration changes made during install
- config_changes Show changes made to server config during installation
- plugins Display information about installed plugins
+# This section is used for --help and --help all ; it needs information
+# about installed plugins to be fully formatted
+COMMAND_OVERVIEW = """The most common SUBCOMMANDS and flags are:
-""".format(cli_command)
-
-# This is the short help for certbot --help, where we disable argparse
-# altogether
-USAGE = SHORT_USAGE + """Choice of server plugins for obtaining and installing cert:
+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
%s
--standalone Run a standalone webserver for authentication
%s
--webroot Place files in a server's webroot folder for authentication
+ --manual Obtain certs interactively, or using shell script hooks
-OR use different plugins to obtain (authenticate) the cert and then install it:
+ -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
- --authenticator standalone --installer apache
+manage certificates:
+ certificates Display information about certs you have from Certbot
+ revoke Revoke a certificate (supply --cert-path)
+ delete Delete a certificate
+manage your account with Let's Encrypt:
+ register Create a Let's Encrypt ACME account
+ --agree-tos Agree to the ACME server's Subscriber Agreement
+ -m EMAIL Email address for important account notifications
+"""
+
+# This is the short help for certbot --help, where we disable argparse
+# altogether
+HELP_USAGE = """
More detailed help:
- -h, --help [topic] print this message, or detailed help on a topic;
- the available topics are:
+ -h, --help [TOPIC] print this message, or detailed help on a topic;
+ the available TOPICS are:
- all, automation, paths, security, testing, or any of the subcommands or
- plugins (certonly, renew, install, register, nginx, apache, standalone,
- webroot, etc.)
+ all, automation, commands, paths, security, testing, or any of the
+ subcommands or plugins (certonly, renew, install, register, nginx,
+ apache, standalone, webroot, etc.)
"""
@@ -137,19 +144,6 @@ def report_config_interaction(modified, modifiers):
VAR_MODIFIERS.setdefault(var, set()).update(modifiers)
-def usage_strings(plugins):
- """Make usage strings late so that plugins can be initialised late"""
- if "nginx" in plugins:
- nginx_doc = "--nginx Use the Nginx plugin for authentication & installation"
- else:
- nginx_doc = "(nginx support is experimental, buggy, and not installed by default)"
- if "apache" in plugins:
- apache_doc = "--apache Use the Apache plugin for authentication & installation"
- else:
- apache_doc = "(the apache plugin is not installed)"
- return USAGE % (apache_doc, nginx_doc), SHORT_USAGE
-
-
def possible_deprecation_warning(config):
"A deprecation warning for users with the old, not-self-upgrading letsencrypt-auto."
if cli_command != LEAUTO:
@@ -305,6 +299,100 @@ class HelpfulArgumentGroup(object):
"""Add a new command line argument to the argument group."""
self._parser.add(self._topic, *args, **kwargs)
+class CustomHelpFormatter(argparse.HelpFormatter):
+ """This is a clone of ArgumentDefaultsHelpFormatter, with bugfixes.
+
+ In particular we fix https://bugs.python.org/issue28742
+ """
+
+ def _get_help_string(self, action):
+ helpstr = action.help
+ if '%(default)' not in action.help and '(default:' not in action.help:
+ if action.default != argparse.SUPPRESS:
+ defaulting_nargs = [argparse.OPTIONAL, argparse.ZERO_OR_MORE]
+ if action.option_strings or action.nargs in defaulting_nargs:
+ helpstr += ' (default: %(default)s)'
+ return helpstr
+
+# The attributes here are:
+# short: a string that will be displayed by "certbot -h commands"
+# opts: a string that heads the section of flags with which this command is documented,
+# both for "cerbot -h SUBCOMMAND" and "certbot -h all"
+# usage: an optional string that overrides the header of "certbot -h SUBCOMMAND"
+VERB_HELP = [
+ ("run (default)", {
+ "short": "Obtain/renew a certificate, and install it",
+ "opts": "Options for obtaining & installing certs",
+ "usage": SHORT_USAGE.replace("[SUBCOMMAND]", ""),
+ "realname": "run"
+ }),
+ ("certonly", {
+ "short": "Obtain or renew a certificate, but do not install it",
+ "opts": "Options for modifying how a cert is obtained",
+ "usage": ("\n\n certbot certonly [options] [-d DOMAIN] [-d DOMAIN] ...\n\n"
+ "This command obtains a TLS/SSL certificate without installing it anywhere.")
+ }),
+ ("renew", {
+ "short": "Renew all certificates (or one specifed with --cert-name)",
+ "opts": ("The 'renew' subcommand will attempt to renew all"
+ " certificates (or more precisely, certificate lineages) you have"
+ " previously obtained if they are close to expiry, and print a"
+ " summary of the results. By default, 'renew' will reuse the options"
+ " used to create obtain or most recently successfully renew each"
+ " certificate lineage. You can try it with `--dry-run` first. For"
+ " more fine-grained control, you can renew individual lineages with"
+ " the `certonly` subcommand. Hooks are available to run commands"
+ " before and after renewal; see"
+ " https://certbot.eff.org/docs/using.html#renewal for more"
+ " information on these."),
+ "usage": "\n\n certbot renew [--cert-name NAME] [options]\n\n"
+ }),
+ ("certificates", {
+ "short": "List certificates managed by Certbot",
+ "opts": "List certificates managed by Certbot",
+ "usage": ("\n\n certbot certificates [options] ...\n\n"
+ "Print information about the status of certificates managed by Certbot.")
+ }),
+ ("delete", {
+ "short": "Clean up all files related to a certificate",
+ "opts": "Options for deleting a certificate"
+ }),
+ ("revoke", {
+ "short": "Revoke a certificate specified with --cert-path",
+ "opts": "Options for revocation of certs",
+ "usage": "\n\n certbot revoke --cert-path /path/to/fullchain.pem [options]\n\n"
+ }),
+ ("register", {
+ "short": "Register for account with Let's Encrypt / other ACME server",
+ "opts": "Options for account registration & modification"
+ }),
+ ("install", {
+ "short": "Install an arbitrary cert in a server",
+ "opts": "Options for modifying how a cert is deployed"
+ }),
+ ("config_changes", {
+ "short": "Show changes that Certbot has made to server configurations",
+ "opts": "Options for controlling which changes are displayed"
+ }),
+ ("rollback", {
+ "short": "Roll back server conf changes made during cert installation",
+ "opts": "Options for rolling back server configuration changes"
+ }),
+ ("plugins", {
+ "short": "List plugins that are installed and available on your system",
+ "opts": 'Options for for the "plugins" subcommand'
+ }),
+ ("update_symlinks", {
+ "short": "Recreate symlinks in your /etc/letsencrypt/live/ directory",
+ "opts": ("Recreates cert 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")))
+ }),
+
+]
+# VERB_HELP is a list in order to preserve order, but a dict is sometimes useful
+VERB_HELP_MAP = dict(VERB_HELP)
+
class HelpfulArgumentParser(object):
"""Argparse Wrapper.
@@ -315,32 +403,34 @@ class HelpfulArgumentParser(object):
"""
+
def __init__(self, args, plugins, detect_defaults=False):
from certbot import main
- self.VERBS = {"auth": main.obtain_cert, "certonly": main.obtain_cert,
- "config_changes": main.config_changes, "run": main.run,
- "install": main.install, "plugins": main.plugins_cmd,
- "register": main.register, "renew": main.renew,
- "revoke": main.revoke, "rollback": main.rollback,
- "everything": main.run}
+ self.VERBS = {
+ "auth": main.obtain_cert,
+ "certonly": main.obtain_cert,
+ "config_changes": main.config_changes,
+ "run": main.run,
+ "install": main.install,
+ "plugins": main.plugins_cmd,
+ "register": main.register,
+ "renew": main.renew,
+ "revoke": main.revoke,
+ "rollback": main.rollback,
+ "everything": main.run,
+ "update_symlinks": main.update_symlinks,
+ "certificates": main.certificates,
+ "delete": main.delete,
+ }
# 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"]
plugin_names = list(plugins)
self.help_topics = HELP_TOPICS + plugin_names + [None]
- usage, short_usage = usage_strings(plugins)
- self.parser = configargparse.ArgParser(
- usage=short_usage,
- formatter_class=argparse.ArgumentDefaultsHelpFormatter,
- args_for_setting_config_path=["-c", "--config"],
- default_config_files=flag_default("config_files"))
-
- # This is the only way to turn off overly verbose config flag documentation
- self.parser._add_config_file_help = False # pylint: disable=protected-access
self.detect_defaults = detect_defaults
-
self.args = args
self.determine_verb()
help1 = self.prescan_for_flag("-h", self.help_topics)
@@ -349,13 +439,72 @@ class HelpfulArgumentParser(object):
self.help_arg = help1 or help2
else:
self.help_arg = help1 if isinstance(help1, str) else help2
- if self.help_arg is True:
- # just --help with no topic; avoid argparse altogether
- print(usage)
- sys.exit(0)
+
+ short_usage = self._usage_string(plugins, self.help_arg)
+
self.visible_topics = self.determine_help_topics(self.help_arg)
self.groups = {} # elements are added by .add_group()
- self.defaults = {} # elements are added by .parse_args()
+ self.defaults = {} # elements are added by .parse_args()
+
+ self.parser = configargparse.ArgParser(
+ prog="certbot",
+ usage=short_usage,
+ formatter_class=CustomHelpFormatter,
+ args_for_setting_config_path=["-c", "--config"],
+ default_config_files=flag_default("config_files"),
+ config_arg_help_message="path to config file (default: {0})".format(
+ " and ".join(flag_default("config_files"))))
+
+ # This is the only way to turn off overly verbose config flag documentation
+ self.parser._add_config_file_help = False # pylint: disable=protected-access
+
+ # Help that are synonyms for --help subcommands
+ COMMANDS_TOPICS = ["command", "commands", "subcommand", "subcommands", "verbs"]
+ def _list_subcommands(self):
+ longest = max(len(v) for v in VERB_HELP_MAP.keys())
+
+ text = "The full list of available SUBCOMMANDS is:\n\n"
+ for verb, props in sorted(VERB_HELP):
+ doc = props.get("short", "")
+ text += '{0:<{length}} {1}\n'.format(verb, doc, length=longest)
+
+ text += "\nYou can get more help on a specific subcommand with --help SUBCOMMAND\n"
+ return text
+
+ def _usage_string(self, plugins, help_arg):
+ """Make usage strings late so that plugins can be initialised late
+
+ :param plugins: all discovered plugins
+ :param help_arg: False for none; True for --help; "TOPIC" for --help TOPIC
+ :rtype: str
+ :returns: a short usage string for the top of --help TOPIC)
+ """
+ if "nginx" in plugins:
+ nginx_doc = "--nginx Use the Nginx plugin for authentication & installation"
+ else:
+ nginx_doc = "(the certbot nginx plugin is not installed)"
+ if "apache" in plugins:
+ apache_doc = "--apache Use the Apache plugin for authentication & installation"
+ else:
+ apache_doc = "(the cerbot apache plugin is not installed)"
+
+ usage = SHORT_USAGE
+ if help_arg == True:
+ print(usage + COMMAND_OVERVIEW % (apache_doc, nginx_doc) + HELP_USAGE)
+ sys.exit(0)
+ elif help_arg in self.COMMANDS_TOPICS:
+ print(usage + self._list_subcommands())
+ sys.exit(0)
+ elif help_arg == "all":
+ # if we're doing --help all, the OVERVIEW is part of the SHORT_USAGE at
+ # the top; if we're doing --help someothertopic, it's OT so it's not
+ usage += COMMAND_OVERVIEW % (apache_doc, nginx_doc)
+ else:
+ custom = VERB_HELP_MAP.get(help_arg, {}).get("usage", None)
+ usage = custom if custom else usage
+
+ return usage
+
def parse_args(self):
"""Parses command line arguments and returns the result.
@@ -376,9 +525,18 @@ class HelpfulArgumentParser(object):
# Do any post-parsing homework here
- if self.verb == "renew" and not parsed_args.dialog_mode:
+ if self.verb == "renew":
+ if parsed_args.force_interactive:
+ raise errors.Error(
+ "{0} cannot be used with renew".format(
+ constants.FORCE_INTERACTIVE_FLAG))
parsed_args.noninteractive_mode = True
+ if parsed_args.force_interactive and parsed_args.noninteractive_mode:
+ raise errors.Error(
+ "Flag for non-interactive mode and {0} conflict".format(
+ constants.FORCE_INTERACTIVE_FLAG))
+
if parsed_args.staging or parsed_args.dry_run:
self.set_test_server(parsed_args)
@@ -388,20 +546,6 @@ class HelpfulArgumentParser(object):
if parsed_args.must_staple:
parsed_args.staple = True
- # Avoid conflicting args
- conficting_args = ["quiet", "noninteractive_mode", "text_mode"]
- if parsed_args.dialog_mode:
- for arg in conficting_args:
- if getattr(parsed_args, arg):
- raise errors.Error(
- ("Conflicting values for displayer."
- " {0} conflicts with dialog_mode").format(arg))
- elif parsed_args.verbose_count > flag_default("verbose_count"):
- parsed_args.text_mode = True
-
- if parsed_args.validate_hooks:
- hooks.validate_hooks(parsed_args)
-
return parsed_args
def set_test_server(self, parsed_args):
@@ -571,7 +715,7 @@ class HelpfulArgumentParser(object):
util.add_deprecated_argument(
self.parser.add_argument, argument_name, num_args)
- def add_group(self, topic, **kwargs):
+ def add_group(self, topic, verbs=(), **kwargs):
"""Create a new argument group.
This method must be called once for every topic, however, calls
@@ -579,6 +723,8 @@ class HelpfulArgumentParser(object):
clarity.
:param str topic: Name of the new argument group.
+ :param str verbs: List of subcommands that should be documented as part of
+ this help group / topic
:returns: The new argument group.
:rtype: `HelpfulArgumentGroup`
@@ -586,6 +732,9 @@ class HelpfulArgumentParser(object):
"""
if self.visible_topics[topic]:
self.groups[topic] = self.parser.add_argument_group(topic, **kwargs)
+ if self.help_arg:
+ for v in verbs:
+ self.groups[topic].add_argument(v, help=VERB_HELP_MAP[v]["short"])
return HelpfulArgumentGroup(self, topic)
@@ -597,7 +746,8 @@ class HelpfulArgumentParser(object):
"""
for name, plugin_ep in six.iteritems(plugins):
- parser_or_group = self.add_group(name, description=plugin_ep.description)
+ parser_or_group = self.add_group(name,
+ description=plugin_ep.long_description)
plugin_ep.plugin_cls.inject_parser_options(parser_or_group, name)
def determine_help_topics(self, chosen_topic):
@@ -625,32 +775,17 @@ class HelpfulArgumentParser(object):
def _add_all_groups(helpful):
helpful.add_group("automation", description="Arguments for automating execution & other tweaks")
helpful.add_group("security", description="Security parameters & server settings")
- helpful.add_group(
- "testing", description="The following flags are meant for "
- "testing purposes only! Do NOT change them, unless you "
- "really know what you're doing!")
- # VERBS
- helpful.add_group(
- "renew", description="The 'renew' subcommand will attempt to renew all"
- " certificates (or more precisely, certificate lineages) you have"
- " previously obtained if they are close to expiry, and print a"
- " summary of the results. By default, 'renew' will reuse the options"
- " used to create obtain or most recently successfully renew each"
- " certificate lineage. You can try it with `--dry-run` first. For"
- " more fine-grained control, you can renew individual lineages with"
- " the `certonly` subcommand. Hooks are available to run commands"
- " before and after renewal; see"
- " https://certbot.eff.org/docs/using.html#renewal for more"
- " information on these.")
-
- helpful.add_group("certonly", description="Options for modifying how a cert is obtained")
- helpful.add_group("install", description="Options for modifying how a cert is deployed")
- helpful.add_group("revoke", description="Options for revocation of certs")
- helpful.add_group("rollback", description="Options for reverting config changes")
- helpful.add_group("plugins", description='Options for the "plugins" subcommand')
- helpful.add_group("config_changes",
- description="Options for showing a history of config changes")
+ helpful.add_group("testing",
+ description="The following flags are meant for testing and integration purposes only.")
helpful.add_group("paths", description="Arguments changing execution paths & servers")
+ helpful.add_group("manage",
+ description="Various subcommands and flags are available for managing your certificates:",
+ verbs=["certificates", "delete", "renew", "revoke", "update_symlinks"])
+
+ # VERBS
+ for verb, docs in VERB_HELP:
+ name = docs.get("realname", verb)
+ helpful.add_group(name, description=docs["opts"])
def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: disable=too-many-statements
@@ -677,24 +812,33 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis
"e.g. -vvv.")
helpful.add(
None, "-t", "--text", dest="text_mode", action="store_true",
- help="Use the text output instead of the curses UI.")
+ help=argparse.SUPPRESS)
helpful.add(
- [None, "automation"], "-n", "--non-interactive", "--noninteractive",
+ [None, "automation", "run", "certonly"], "-n", "--non-interactive", "--noninteractive",
dest="noninteractive_mode", action="store_true",
help="Run without ever asking for user input. This may require "
"additional command line flags; the client will try to explain "
"which ones are required if it finds one missing")
helpful.add(
- None, "--dialog", dest="dialog_mode", action="store_true",
- help="Run using interactive dialog menus")
+ [None, "register", "run", "certonly"],
+ constants.FORCE_INTERACTIVE_FLAG, action="store_true",
+ help="Force Certbot to be interactive even if it detects it's not "
+ "being run in a terminal. This flag cannot be used with the "
+ "renew subcommand.")
helpful.add(
- [None, "run", "certonly"],
+ [None, "run", "certonly", "certificates"],
"-d", "--domains", "--domain", dest="domains",
metavar="DOMAIN", action=_DomainsAction, default=[],
help="Domain names to apply. For multiple domains you can use "
"multiple -d flags or enter a comma separated list of domains "
- "as a parameter.")
-
+ "as a parameter. (default: Ask)")
+ helpful.add(
+ [None, "run", "certonly", "manage", "delete", "certificates"],
+ "--cert-name", dest="certname",
+ metavar="CERTNAME", default=None,
+ help="Certificate name to apply. Only one certificate name can be used "
+ "per Certbot run. To see certificate names, run 'certbot certificates'. "
+ "When creating a new certificate, specifies the new certificate's name.")
helpful.add(
[None, "testing", "renew", "certonly"],
"--dry-run", action="store_true", dest="dry_run",
@@ -723,18 +867,18 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis
help="With the register verb, indicates that details associated "
"with an existing registration, such as the e-mail address, "
"should be updated, rather than registering a new account.")
- helpful.add(None, "-m", "--email", help=config_help("email"))
+ helpful.add(["register", "automation"], "-m", "--email", help=config_help("email"))
helpful.add(
- ["automation", "renew", "certonly", "run"],
+ ["automation", "certonly", "run"],
"--keep-until-expiring", "--keep", "--reinstall",
dest="reinstall", action="store_true",
help="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)")
+ "'run' subcommand this means reinstall the existing cert). (default: Ask)")
helpful.add(
"automation", "--expand", action="store_true",
help="If an existing cert covers some subset of the requested names, "
- "always expand and replace it with the additional names.")
+ "always expand and replace it with the additional names. (default: Ask)")
helpful.add(
"automation", "--version", action="version",
version="%(prog)s {0}".format(certbot.__version__),
@@ -747,6 +891,12 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis
"regardless of whether it is near expiry. (Often "
"--keep-until-expiring is more appropriate). Also implies "
"--expand.")
+ helpful.add(
+ "automation", "--renew-with-new-domains",
+ action="store_true", dest="renew_with_new_domains", help="If a "
+ "certificate already exists for the requested certificate name "
+ "but does not match the requested domains, renew it now, "
+ "regardless of whether it is near expiry.")
helpful.add(
["automation", "renew", "certonly"],
"--allow-subset-of-names", action="store_true",
@@ -757,7 +907,7 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis
"at this system. This option cannot be used with --csr.")
helpful.add(
"automation", "--agree-tos", dest="tos", action="store_true",
- help="Agree to the ACME Subscriber Agreement")
+ help="Agree to the ACME Subscriber Agreement (default: Ask)")
helpful.add(
"automation", "--account", metavar="ACCOUNT_ID",
help="Account ID to use")
@@ -771,15 +921,17 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis
helpful.add(
"automation", "--no-self-upgrade", action="store_true",
help="(certbot-auto only) prevent the certbot-auto script from"
- " upgrading itself to newer released versions")
+ " upgrading itself to newer released versions (default: Upgrade"
+ " automatically)")
helpful.add(
- ["automation", "renew", "certonly"],
+ ["automation", "renew", "certonly", "run"],
"-q", "--quiet", dest="quiet", action="store_true",
help="Silence all output except errors. Useful for automation via cron."
" Implies --non-interactive.")
# overwrites server, handled in HelpfulArgumentParser.parse_args()
- helpful.add("testing", "--test-cert", "--staging", action='store_true', dest='staging',
- help='Use the staging server to obtain test (invalid) certs; equivalent'
+ helpful.add(["testing", "revoke", "run"], "--test-cert", "--staging",
+ action='store_true', dest='staging',
+ help='Use the staging server to obtain or revoke test (invalid) certs; equivalent'
' to --server ' + constants.STAGING_URI)
helpful.add(
"testing", "--debug", action="store_true",
@@ -790,11 +942,11 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis
help=config_help("no_verify_ssl"),
default=flag_default("no_verify_ssl"))
helpful.add(
- ["certonly", "renew", "run"], "--tls-sni-01-port", type=int,
+ ["testing", "standalone", "apache", "nginx"], "--tls-sni-01-port", type=int,
default=flag_default("tls_sni_01_port"),
help=config_help("tls_sni_01_port"))
helpful.add(
- ["certonly", "renew", "run", "manual"], "--http-01-port", type=int,
+ ["testing", "standalone", "manual"], "--http-01-port", type=int,
dest="http01_port",
default=flag_default("http01_port"), help=config_help("http01_port"))
helpful.add(
@@ -810,11 +962,11 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis
helpful.add(
"security", "--redirect", action="store_true",
help="Automatically redirect all HTTP traffic to HTTPS for the newly "
- "authenticated vhost.", dest="redirect", default=None)
+ "authenticated vhost. (default: Ask)", dest="redirect", default=None)
helpful.add(
"security", "--no-redirect", action="store_false",
help="Do not automatically redirect all HTTP traffic to HTTPS for the newly "
- "authenticated vhost.", dest="redirect", default=None)
+ "authenticated vhost. (default: Ask)", dest="redirect", default=None)
helpful.add(
"security", "--hsts", action="store_true",
help="Add the Strict-Transport-Security header to every HTTP response."
@@ -822,8 +974,7 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis
" Defends against SSL Stripping.", dest="hsts", default=False)
helpful.add(
"security", "--no-hsts", action="store_false",
- help="Do not automatically add the Strict-Transport-Security header"
- " to every HTTP response.", dest="hsts", default=False)
+ help=argparse.SUPPRESS, dest="hsts", default=False)
helpful.add(
"security", "--uir", action="store_true",
help="Add the \"Content-Security-Policy: upgrade-insecure-requests\""
@@ -831,9 +982,7 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis
" https:// for every http:// resource.", dest="uir", default=None)
helpful.add(
"security", "--no-uir", action="store_false",
- help="Do not automatically set the \"Content-Security-Policy:"
- " upgrade-insecure-requests\" header to every HTTP response.",
- dest="uir", default=None)
+ help=argparse.SUPPRESS, dest="uir", default=None)
helpful.add(
"security", "--staple-ocsp", action="store_true",
help="Enables OCSP Stapling. A valid OCSP response is stapled to"
@@ -841,14 +990,13 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis
dest="staple", default=None)
helpful.add(
"security", "--no-staple-ocsp", action="store_false",
- help="Do not automatically enable OCSP Stapling.",
- dest="staple", default=None)
+ help=argparse.SUPPRESS, dest="staple", default=None)
helpful.add(
"security", "--strict-permissions", action="store_true",
help="Require that all configuration files are owned by the current "
"user; only needed if your config is somewhere unsafe like /tmp/")
helpful.add(
- ["manual", "standalone", "certonly", "renew", "run"],
+ ["manual", "standalone", "certonly", "renew"],
"--preferred-challenges", dest="pref_challs",
action=_PrefChallAction, default=[],
help='A sorted, comma delimited list of the preferred challenge to '
@@ -865,13 +1013,16 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis
" Intended primarily for renewal, where it can be used to temporarily"
" shut down a webserver that might conflict with the standalone"
" plugin. This will only be called if a certificate is actually to be"
- " obtained/renewed.")
+ " obtained/renewed. When renewing several certificates that have"
+ " identical pre-hooks, only the first will be executed.")
helpful.add(
"renew", "--post-hook",
help="Command to be run in a shell after attempting to obtain/renew"
" certificates. Can be used to deploy renewed certificates, or to"
" restart any servers that were stopped by --pre-hook. This is only"
- " run if an attempt was made to obtain/renew a certificate.")
+ " 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",
help="Command to be run in a shell once for each successfully renewed"
@@ -887,9 +1038,11 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis
" 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.")
+ " shell constructs, so you can use this switch to disable it."
+ " (default: False)")
helpful.add_deprecated_argument("--agree-dev-preview", 0)
+ helpful.add_deprecated_argument("--dialog", 0)
_create_subparsers(helpful)
_paths_parser(helpful)
@@ -906,18 +1059,24 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis
def _create_subparsers(helpful):
helpful.add("config_changes", "--num", type=int,
help="How many past revisions you want to be displayed")
+
+ from certbot.client import sample_user_agent # avoid import loops
helpful.add(
None, "--user-agent", default=None,
help="Set a custom user agent string for the client. User agent strings allow "
"the CA to collect high level statistics about success rates by OS and "
"plugin. If you wish to hide your server OS version from the Let's "
- 'Encrypt server, set this to "".')
+ 'Encrypt server, set this to "". '
+ '(default: {0})'.format(sample_user_agent()))
helpful.add("certonly",
"--csr", type=read_file,
- help="Path to a Certificate Signing Request (CSR) in DER"
- " format; note that the .csr file *must* contain a Subject"
- " Alternative Name field for each domain you want certified."
- " Currently --csr only works with the 'certonly' subcommand'")
+ help="Path to a Certificate Signing Request (CSR) in DER or PEM format."
+ " Currently --csr only works with the 'certonly' subcommand.")
+ helpful.add("revoke",
+ "--reason", dest="reason",
+ choices=CaseInsensitiveList(constants.REVOCATION_REASONS.keys()),
+ action=_EncodeReasonAction, default=0,
+ help="Specify reason for revoking certificate.")
helpful.add("rollback",
"--checkpoints", type=int, metavar="N",
default=flag_default("rollback_checkpoints"),
@@ -934,16 +1093,24 @@ def _create_subparsers(helpful):
const=interfaces.IInstaller, help="Limit to installer plugins only.")
+class CaseInsensitiveList(list):
+ """A list that will ignore case when searching.
+
+ This class is passed to the `choices` argument of `argparse.add_arguments`
+ through the `helpful` wrapper. It is necessary due to special handling of
+ command line arguments by `set_by_cli` in which the `type_func` is not applied."""
+ def __contains__(self, element):
+ return super(CaseInsensitiveList, self).__contains__(element.lower())
+
+
def _paths_parser(helpful):
add = helpful.add
verb = helpful.verb
if verb == "help":
verb = helpful.help_arg
- cph = "Path to where cert is saved (with auth --csr), installed from or revoked."
- section = "paths"
- if verb in ("install", "revoke", "certonly"):
- section = verb
+ cph = "Path to where cert is saved (with auth --csr), installed from, or revoked."
+ section = ["paths", "install", "revoke", "certonly", "manage"]
if verb == "certonly":
add(section, "--cert-path", type=os.path.abspath,
default=flag_default("auth_cert_path"), help=cph)
@@ -965,7 +1132,7 @@ def _paths_parser(helpful):
default_cp = None
if verb == "certonly":
default_cp = flag_default("auth_chain_path")
- add("paths", "--fullchain-path", default=default_cp, type=os.path.abspath,
+ add(["install", "paths"], "--fullchain-path", default=default_cp, type=os.path.abspath,
help="Accompanying path to a full certificate chain (cert plus chain).")
add("paths", "--chain-path", default=default_cp, type=os.path.abspath,
help="Accompanying path to a certificate chain.")
@@ -988,20 +1155,18 @@ def _plugins_parsing(helpful, plugins):
"a particular plugin by setting options provided below. Running "
"--help will list flags specific to that plugin.")
- helpful.add(
- "plugins", "-a", "--authenticator", help="Authenticator plugin name.")
- helpful.add(
- "plugins", "-i", "--installer", help="Installer plugin name (also used to find domains).")
- helpful.add(
- "plugins", "--configurator", help="Name of the plugin that is "
- "both an authenticator and an installer. Should not be used "
- "together with --authenticator or --installer.")
- helpful.add(["plugins", "certonly", "run", "install"],
+ helpful.add("plugins", "--configurator",
+ help="Name of the plugin that is both an authenticator and an installer."
+ " Should not be used together with --authenticator or --installer. "
+ "(default: Ask)")
+ helpful.add("plugins", "-a", "--authenticator", help="Authenticator plugin name.")
+ helpful.add("plugins", "-i", "--installer",
+ help="Installer plugin name (also used to find domains).")
+ helpful.add(["plugins", "certonly", "run", "install", "config_changes"],
"--apache", action="store_true",
help="Obtain and install certs using Apache")
- helpful.add(["plugins", "certonly", "run", "install"],
- "--nginx", action="store_true",
- help="Obtain and install certs using Nginx")
+ helpful.add(["plugins", "certonly", "run", "install", "config_changes"],
+ "--nginx", action="store_true", help="Obtain and install certs using Nginx")
helpful.add(["plugins", "certonly"], "--standalone", action="store_true",
help='Obtain certs using a "standalone" webserver.')
helpful.add(["plugins", "certonly"], "--manual", action="store_true",
@@ -1016,6 +1181,15 @@ def _plugins_parsing(helpful, plugins):
helpful.add_plugin_args(plugins)
+class _EncodeReasonAction(argparse.Action):
+ """Action class for parsing revocation reason."""
+
+ def __call__(self, parser, namespace, reason, option_string=None):
+ """Encodes the reason for certificate revocation."""
+ code = constants.REVOCATION_REASONS[reason.lower()]
+ setattr(namespace, self.dest, code)
+
+
class _DomainsAction(argparse.Action):
"""Action class for parsing domains."""
@@ -1023,7 +1197,6 @@ class _DomainsAction(argparse.Action):
"""Just wrap add_domains in argparseese."""
add_domains(namespace, domain)
-
def add_domains(args_or_config, domains):
"""Registers new domains to be used during the current client run.
diff --git a/certbot/client.py b/certbot/client.py
index 55f3d5e67..b40a169e6 100644
--- a/certbot/client.py
+++ b/certbot/client.py
@@ -15,7 +15,6 @@ import certbot
from certbot import account
from certbot import auth_handler
-from certbot import configuration
from certbot import constants
from certbot import crypto_util
from certbot import errors
@@ -38,11 +37,11 @@ def acme_from_config_key(config, key):
"Wrangle ACME client construction"
# TODO: Allow for other alg types besides RS256
net = acme_client.ClientNetwork(key, verify_ssl=(not config.no_verify_ssl),
- user_agent=_determine_user_agent(config))
+ user_agent=determine_user_agent(config))
return acme_client.Client(config.server, key=key, net=net)
-def _determine_user_agent(config):
+def determine_user_agent(config):
"""
Set a user_agent string in the config based on the choice of plugins.
(this wasn't knowable at construction time)
@@ -59,6 +58,16 @@ def _determine_user_agent(config):
ua = config.user_agent
return ua
+def sample_user_agent():
+ "Document what this Certbot's user agent string will be like."
+ class DummyConfig(object):
+ "Shim for computing a sample user agent."
+ def __init__(self):
+ self.authenticator = "XXX"
+ self.installer = "YYY"
+ self.user_agent = None
+ return determine_user_agent(DummyConfig())
+
def register(config, account_storage, tos_cb=None):
"""Register new account with an ACME CA.
@@ -263,7 +272,7 @@ class Client(object):
return (self.obtain_certificate_from_csr(domains, csr, authzr=authzr)
+ (key, csr))
- def obtain_and_enroll_certificate(self, domains):
+ def obtain_and_enroll_certificate(self, domains, certname):
"""Obtain and enroll certificate.
Get a new certificate for the specified domains using the specified
@@ -272,6 +281,7 @@ class Client(object):
:param list domains: Domains to request.
:param plugins: A PluginsFactory object.
+ :param str certname: Name of new cert
:returns: A new :class:`certbot.storage.RenewableCert` instance
referred to the enrolled cert lineage, False if the cert could not
@@ -286,16 +296,17 @@ class Client(object):
"Non-standard path(s), might not work with crontab installed "
"by your operating system package manager")
+ new_name = certname if certname else domains[0]
if self.config.dry_run:
logger.debug("Dry run: Skipping creating new lineage for %s",
- domains[0])
+ new_name)
return None
else:
return storage.RenewableCert.new_lineage(
- domains[0], OpenSSL.crypto.dump_certificate(
+ new_name, OpenSSL.crypto.dump_certificate(
OpenSSL.crypto.FILETYPE_PEM, certr.body.wrapped),
key.pem, crypto_util.dump_pyopenssl_chain(chain),
- configuration.RenewerConfiguration(self.config.namespace))
+ self.config)
def save_certificate(self, certr, chain_cert,
cert_path, chain_path, fullchain_path):
@@ -322,7 +333,7 @@ class Client(object):
self.config.strict_permissions)
cert_pem = OpenSSL.crypto.dump_certificate(
- OpenSSL.crypto.FILETYPE_PEM, certr.body.wrapped).decode('ascii')
+ OpenSSL.crypto.FILETYPE_PEM, certr.body.wrapped)
cert_file, abs_cert_path = _open_pem_file('cert_path', cert_path)
@@ -365,7 +376,8 @@ class Client(object):
chain_path = None if chain_path is None else os.path.abspath(chain_path)
- with error_handler.ErrorHandler(self.installer.recovery_routine):
+ msg = ("Unable to install the certificate")
+ with error_handler.ErrorHandler(self._recovery_routine_with_msg, msg):
for dom in domains:
self.installer.deploy_cert(
domain=dom, cert_path=os.path.abspath(cert_path),
@@ -383,16 +395,10 @@ class Client(object):
# sites may have been enabled / final cleanup
self.installer.restart()
- def enhance_config(self, domains, config, chain_path):
+ def enhance_config(self, domains, chain_path):
"""Enhance the configuration.
:param list domains: list of domains to configure
-
- :ivar config: Namespace typically produced by
- :meth:`argparse.ArgumentParser.parse_args`.
- it must have the redirect, hsts and uir attributes.
- :type namespace: :class:`argparse.Namespace`
-
:param chain_path: chain file path
:type chain_path: `str` or `None`
@@ -400,39 +406,34 @@ class Client(object):
client.
"""
-
if self.installer is None:
logger.warning("No installer is specified, there isn't any "
"configuration to enhance.")
raise errors.Error("No installer available")
- if config is None:
- logger.warning("No config is specified.")
- raise errors.Error("No config available")
-
+ enhanced = False
+ enhancement_info = (
+ ("hsts", "ensure-http-header", "Strict-Transport-Security"),
+ ("redirect", "redirect", None),
+ ("staple", "staple-ocsp", chain_path),
+ ("uir", "ensure-http-header", "Upgrade-Insecure-Requests"),)
supported = self.installer.supported_enhancements()
- redirect = config.redirect if "redirect" in supported else False
- hsts = config.hsts if "ensure-http-header" in supported else False
- uir = config.uir if "ensure-http-header" in supported else False
- staple = config.staple if "staple-ocsp" in supported else False
- if redirect is None:
- redirect = enhancements.ask("redirect")
-
- if redirect:
- self.apply_enhancement(domains, "redirect")
-
- if hsts:
- self.apply_enhancement(domains, "ensure-http-header",
- "Strict-Transport-Security")
- if uir:
- self.apply_enhancement(domains, "ensure-http-header",
- "Upgrade-Insecure-Requests")
- if staple:
- self.apply_enhancement(domains, "staple-ocsp", chain_path)
+ for config_name, enhancement_name, option in enhancement_info:
+ config_value = getattr(self.config, config_name)
+ if enhancement_name in supported:
+ if config_name == "redirect" and config_value is None:
+ config_value = enhancements.ask(enhancement_name)
+ if config_value:
+ self.apply_enhancement(domains, enhancement_name, option)
+ enhanced = True
+ elif config_value:
+ logger.warning(
+ "Option %s is not supported by the selected installer. "
+ "Skipping enhancement.", config_name)
msg = ("We were unable to restart web server")
- if redirect or hsts or uir or staple:
+ if enhanced:
with error_handler.ErrorHandler(self._rollback_and_restart, msg):
self.installer.restart()
@@ -595,10 +596,10 @@ def _open_pem_file(cli_arg_path, pem_path):
"""
if cli.set_by_cli(cli_arg_path):
- return util.safe_open(pem_path, chmod=0o644),\
+ return util.safe_open(pem_path, chmod=0o644, mode="wb"),\
os.path.abspath(pem_path)
else:
- uniq = util.unique_file(pem_path, 0o644)
+ uniq = util.unique_file(pem_path, 0o644, "wb")
return uniq[0], os.path.abspath(uniq[1])
def _save_chain(chain_pem, chain_file):
diff --git a/certbot/configuration.py b/certbot/configuration.py
index 712135b8d..d25378922 100644
--- a/certbot/configuration.py
+++ b/certbot/configuration.py
@@ -25,9 +25,16 @@ class NamespaceConfig(object):
- `csr_dir`
- `in_progress_dir`
- `key_dir`
- - `renewer_config_file`
- `temp_checkpoint_dir`
+ And the following paths are dynamically resolved using
+ :attr:`~certbot.interfaces.IConfig.config_dir` and relative
+ paths defined in :py:mod:`certbot.constants`:
+
+ - `default_archive_dir`
+ - `live_dir`
+ - `renewal_configs_dir`
+
:ivar namespace: Namespace typically produced by
:meth:`argparse.ArgumentParser.parse_args`.
:type namespace: :class:`argparse.Namespace`
@@ -85,18 +92,8 @@ class NamespaceConfig(object):
new_ns = copy.deepcopy(self.namespace)
return type(self)(new_ns)
-
-class RenewerConfiguration(object):
- """Configuration wrapper for renewer."""
-
- def __init__(self, namespace):
- self.namespace = namespace
-
- def __getattr__(self, name):
- return getattr(self.namespace, name)
-
@property
- def archive_dir(self): # pylint: disable=missing-docstring
+ def default_archive_dir(self): # pylint: disable=missing-docstring
return os.path.join(self.namespace.config_dir, constants.ARCHIVE_DIR)
@property
@@ -108,11 +105,6 @@ class RenewerConfiguration(object):
return os.path.join(
self.namespace.config_dir, constants.RENEWAL_CONFIGS_DIR)
- @property
- def renewer_config_file(self): # pylint: disable=missing-docstring
- return os.path.join(
- self.namespace.config_dir, constants.RENEWER_CONFIG_FILENAME)
-
def check_config_sanity(config):
"""Validate command line options and display error message if
diff --git a/certbot/constants.py b/certbot/constants.py
index 117301380..f64ff7e1e 100644
--- a/certbot/constants.py
+++ b/certbot/constants.py
@@ -35,6 +35,17 @@ CLI_DEFAULTS = dict(
)
STAGING_URI = "https://acme-staging.api.letsencrypt.org/directory"
+# The set of reasons for revoking a certificate is defined in RFC 5280 in
+# section 5.3.1. The reasons that users are allowed to submit are restricted to
+# those accepted by the ACME server implementation. They are listed in
+# `letsencrypt.boulder.revocation.reasons.go`.
+REVOCATION_REASONS = {
+ "unspecified": 0,
+ "keycompromise": 1,
+ "affiliationchanged": 3,
+ "superseded": 4,
+ "cessationofoperation": 5}
+
"""Defaults for CLI flags and `.IConfig` attributes."""
QUIET_LOGGING_LEVEL = logging.WARNING
@@ -93,5 +104,5 @@ TEMP_CHECKPOINT_DIR = "temp_checkpoint"
RENEWAL_CONFIGS_DIR = "renewal"
"""Renewal configs directory, relative to `IConfig.config_dir`."""
-RENEWER_CONFIG_FILENAME = "renewer.conf"
-"""Renewer config file name (relative to `IConfig.config_dir`)."""
+FORCE_INTERACTIVE_FLAG = "--force-interactive"
+"""Flag to disable TTY checking in IDisplay."""
diff --git a/certbot/crypto_util.py b/certbot/crypto_util.py
index 7253742b0..65e3de345 100644
--- a/certbot/crypto_util.py
+++ b/certbot/crypto_util.py
@@ -53,7 +53,8 @@ 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)
- key_f, key_path = util.unique_file(os.path.join(key_dir, keyname), 0o600)
+ key_f, key_path = util.unique_file(
+ os.path.join(key_dir, keyname), 0o600, "wb")
with key_f:
key_f.write(key_pem)
@@ -85,7 +86,7 @@ def init_save_csr(privkey, names, path, csrname="csr-certbot.pem"):
util.make_or_verify_dir(path, 0o755, os.geteuid(),
config.strict_permissions)
csr_f, csr_filename = util.unique_file(
- os.path.join(path, csrname), 0o644)
+ os.path.join(path, csrname), 0o644, "wb")
csr_f.write(csr_pem)
csr_f.close()
@@ -351,11 +352,11 @@ def dump_pyopenssl_chain(chain, filetype=OpenSSL.crypto.FILETYPE_PEM):
if isinstance(cert, jose.ComparableX509):
# pylint: disable=protected-access
cert = cert.wrapped
- return OpenSSL.crypto.dump_certificate(filetype, cert).decode('ascii')
+ return OpenSSL.crypto.dump_certificate(filetype, cert)
# assumes that OpenSSL.crypto.dump_certificate includes ending
# newline character
- return "".join(_dump_cert(cert) for cert in chain)
+ return b"".join(_dump_cert(cert) for cert in chain)
def notBefore(cert_path):
diff --git a/certbot/display/enhancements.py b/certbot/display/enhancements.py
index 3b128a874..d2ffe2e0d 100644
--- a/certbot/display/enhancements.py
+++ b/certbot/display/enhancements.py
@@ -48,7 +48,8 @@ def redirect_by_default():
code, selection = util(interfaces.IDisplay).menu(
"Please choose whether HTTPS access is required or optional.",
- choices, default=0, cli_flag="--redirect / --no-redirect")
+ choices, default=0,
+ cli_flag="--redirect / --no-redirect", force_interactive=True)
if code != display_util.OK:
return False
diff --git a/certbot/display/ops.py b/certbot/display/ops.py
index 662483ee0..85343fdc3 100644
--- a/certbot/display/ops.py
+++ b/certbot/display/ops.py
@@ -37,6 +37,7 @@ def get_email(invalid=False, optional=True):
if optional:
if invalid:
msg += unsafe_suggestion
+ suggest_unsafe = False
else:
suggest_unsafe = True
else:
@@ -45,7 +46,8 @@ def get_email(invalid=False, optional=True):
while True:
try:
code, email = z_util(interfaces.IDisplay).input(
- invalid_prefix + msg if invalid else msg)
+ invalid_prefix + msg if invalid else msg,
+ force_interactive=True)
except errors.MissingCommandlineFlag:
msg = ("You should register before running non-interactively, "
"or provide --agree-tos and --email flags.")
@@ -78,7 +80,7 @@ def choose_account(accounts):
labels = [acc.slug for acc in accounts]
code, index = z_util(interfaces.IDisplay).menu(
- "Please choose an account", labels)
+ "Please choose an account", labels, force_interactive=True)
if code == display_util.OK:
return accounts[index]
else:
@@ -129,6 +131,16 @@ def get_valid_domains(domains):
continue
return valid_domains
+def _sort_names(FQDNs):
+ """Sort FQDNs by SLD (and if many, by their subdomains)
+
+ :param list FQDNs: list of domain names
+
+ :returns: Sorted list of domain names
+ :rtype: list
+ """
+ return sorted(FQDNs, key=lambda fqdn: fqdn.split('.')[::-1][1:])
+
def _filter_names(names):
"""Determine which names the user would like to select from a list.
@@ -141,9 +153,12 @@ def _filter_names(names):
:rtype: tuple
"""
+ #Sort by domain first, and then by subdomain
+ sorted_names = _sort_names(names)
+
code, names = z_util(interfaces.IDisplay).checklist(
"Which names would you like to activate HTTPS for?",
- tags=names, cli_flag="--domains")
+ tags=sorted_names, cli_flag="--domains", force_interactive=True)
return code, [str(s) for s in names]
@@ -159,7 +174,7 @@ def _choose_names_manually(prompt_prefix=""):
code, input_ = z_util(interfaces.IDisplay).input(
prompt_prefix +
"Please enter in your domain name(s) (comma and/or space separated) ",
- cli_flag="--domains")
+ cli_flag="--domains", force_interactive=True)
if code == display_util.OK:
invalid_domains = dict()
@@ -197,7 +212,8 @@ def _choose_names_manually(prompt_prefix=""):
if retry_message:
# We had error in input
- retry = z_util(interfaces.IDisplay).yesno(retry_message)
+ retry = z_util(interfaces.IDisplay).yesno(retry_message,
+ force_interactive=True)
if retry:
return _choose_names_manually()
else:
@@ -208,8 +224,6 @@ def _choose_names_manually(prompt_prefix=""):
def success_installation(domains):
"""Display a box confirming the installation of HTTPS.
- .. todo:: This should be centered on the screen
-
:param list domains: domain names which were enabled
"""
@@ -219,29 +233,36 @@ def success_installation(domains):
_gen_https_names(domains),
os.linesep,
os.linesep.join(_gen_ssl_lab_urls(domains))),
- height=(10 + len(domains)),
pause=False)
-def success_renewal(domains, action):
+def success_renewal(domains):
"""Display a box confirming the renewal of an existing certificate.
- .. todo:: This should be centered on the screen
-
:param list domains: domain names which were renewed
- :param str action: can be "reinstall" or "renew"
"""
z_util(interfaces.IDisplay).notification(
- "Your existing certificate has been successfully {3}ed, and the "
+ "Your existing certificate has been successfully renewed, and the "
"new certificate has been installed.{1}{1}"
"The new certificate covers the following domains: {0}{1}{1}"
"You should test your configuration at:{1}{2}".format(
_gen_https_names(domains),
os.linesep,
- os.linesep.join(_gen_ssl_lab_urls(domains)),
- action),
- height=(14 + len(domains)),
+ os.linesep.join(_gen_ssl_lab_urls(domains))),
+ pause=False)
+
+def success_revocation(cert_path):
+ """Display a box confirming a certificate has been revoked.
+
+ :param list cert_path: path to certificate which was revoked.
+
+ """
+ z_util(interfaces.IDisplay).notification(
+ "Congratulations! You have successfully revoked the certificate "
+ "that was located at {0}{1}{1}".format(
+ cert_path,
+ os.linesep),
pause=False)
diff --git a/certbot/display/util.py b/certbot/display/util.py
index c0e9386cd..87c75739b 100644
--- a/certbot/display/util.py
+++ b/certbot/display/util.py
@@ -2,27 +2,19 @@
import logging
import os
import textwrap
+import sys
-import dialog
import six
import zope.interface
+from certbot import constants
from certbot import interfaces
from certbot import errors
from certbot.display import completer
-
logger = logging.getLogger(__name__)
WIDTH = 72
-HEIGHT = 20
-
-DSELECT_HELP = (
- "Use the arrow keys or Tab to move between window elements. Space can be "
- "used to complete the input path with the selected element in the "
- "directory window. Pressing enter will select the currently highlighted "
- "button.")
-"""Help text on how to use dialog's dselect."""
# Display exit codes
OK = "ok"
@@ -59,198 +51,45 @@ def _wrap_lines(msg):
return os.linesep.join(fixed_l)
-
-def _clean(dialog_result):
- """Treat sundy python-dialog return codes as CANCEL
-
- :param tuple dialog_result: (code, result)
- :returns: the argument but with unknown codes set to -1 (Error)
- :rtype: tuple
- """
- code, result = dialog_result
- if code in (OK, HELP):
- return dialog_result
- elif code in (CANCEL, ESC):
- return (CANCEL, result)
- else:
- logger.debug("Surprising dialog return code %s", code)
- return (CANCEL, result)
-
-
-@zope.interface.implementer(interfaces.IDisplay)
-class NcursesDisplay(object):
- """Ncurses-based display."""
-
- def __init__(self, width=WIDTH, height=HEIGHT):
- super(NcursesDisplay, self).__init__()
- self.dialog = dialog.Dialog(autowidgetsize=True)
- assert OK == self.dialog.DIALOG_OK, "What kind of absurdity is this?"
- self.width = width
- self.height = height
-
- def notification(self, message, height=10, pause=False):
- # pylint: disable=unused-argument
- """Display a notification to the user and wait for user acceptance.
-
- .. todo:: It probably makes sense to use one of the transient message
- types for pause. It isn't straightforward how best to approach
- the matter though given the context of our messages.
- http://pythondialog.sourceforge.net/doc/widgets.html#displaying-transient-messages
-
- :param str message: Message to display
- :param int height: Height of the dialog box
- :param bool pause: Not applicable to NcursesDisplay
-
- """
- self.dialog.msgbox(message)
-
- def menu(self, message, choices, ok_label="OK", cancel_label="Cancel",
- help_label="", **unused_kwargs):
- """Display a menu.
-
- :param str message: title of menu
-
- :param choices: menu lines, len must be > 0
- :type choices: list of tuples (`tag`, `item`) tags must be unique or
- list of items (tags will be enumerated)
-
- :param str ok_label: label of the OK button
- :param str help_label: label of the help button
- :param dict unused_kwargs: absorbs default / cli_args
-
- :returns: tuple of the form (`code`, `index`) where
- `code` - display exit code
- `int` - index of the selected item
- :rtype: tuple
-
- """
- menu_options = {
- "choices": choices,
- "ok_label": ok_label,
- "cancel_label": cancel_label,
- "help_button": bool(help_label),
- "help_label": help_label,
- "width": self.width,
- "height": self.height,
- "menu_height": self.height - 6,
- }
-
- # Can accept either tuples or just the actual choices
- if choices and isinstance(choices[0], tuple):
- # pylint: disable=star-args
- code, selection = _clean(self.dialog.menu(message, **menu_options))
-
- # Return the selection index
- for i, choice in enumerate(choices):
- if choice[0] == selection:
- return code, i
-
- return code, -1
-
- else:
- # "choices" is not formatted the way the dialog.menu expects...
- menu_options["choices"] = [
- (str(i), choice) for i, choice in enumerate(choices, 1)
- ]
- # pylint: disable=star-args
- code, index = _clean(self.dialog.menu(message, **menu_options))
-
- if code == CANCEL or index == "":
- return code, -1
-
- return code, int(index) - 1
-
- def input(self, message, **unused_kwargs):
- """Display an input box to the user.
-
- :param str message: Message to display that asks for input.
- :param dict _kwargs: absorbs default / cli_args
-
- :returns: tuple of the form (`code`, `string`) where
- `code` - display exit code
- `string` - input entered by the user
-
- """
- return self.dialog.inputbox(message)
-
- def yesno(self, message, yes_label="Yes", no_label="No", **unused_kwargs):
- """Display a Yes/No dialog box.
-
- Yes and No label must begin with different letters.
-
- :param str message: message to display to user
- :param str yes_label: label on the "yes" button
- :param str no_label: label on the "no" button
- :param dict _kwargs: absorbs default / cli_args
-
- :returns: if yes_label was selected
- :rtype: bool
-
- """
- return self.dialog.DIALOG_OK == self.dialog.yesno(
- message, yes_label=yes_label, no_label=no_label)
-
- def checklist(self, message, tags, default_status=True, **unused_kwargs):
- """Displays a checklist.
-
- :param message: Message to display before choices
- :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 dict _kwargs: absorbs default / cli_args
-
-
- :returns: tuple of the form (`code`, `list_tags`) where
- `code` - display exit code
- `list_tags` - list of str tags selected by the user
-
- """
- choices = [(tag, "", default_status) for tag in tags]
- return self.dialog.checklist(message, choices=choices)
-
- def directory_select(self, message, **unused_kwargs):
- """Display a directory selection screen.
-
- :param str message: prompt to give the user
-
- :returns: tuple of the form (`code`, `string`) where
- `code` - display exit code
- `string` - input entered by the user
-
- """
- root_directory = os.path.abspath(os.sep)
- return self.dialog.dselect(
- filepath=root_directory, help_button=True, title=message)
-
-
@zope.interface.implementer(interfaces.IDisplay)
class FileDisplay(object):
"""File-based display."""
+ # pylint: disable=too-many-arguments
+ # see https://github.com/certbot/certbot/issues/3915
- def __init__(self, outfile):
+ def __init__(self, outfile, force_interactive):
super(FileDisplay, self).__init__()
self.outfile = outfile
+ self.force_interactive = force_interactive
+ self.skipped_interaction = False
- def notification(self, message, height=10, pause=True):
- # pylint: disable=unused-argument
+ def notification(self, message, pause=True,
+ wrap=True, force_interactive=False):
"""Displays a notification and waits for user acceptance.
:param str message: Message to display
- :param int height: No effect for FileDisplay
:param bool pause: Whether or not the program should pause for the
user's confirmation
+ :param bool wrap: Whether or not the application should wrap text
+ :param bool force_interactive: True if it's safe to prompt the user
+ because it won't cause any workflow regressions
"""
side_frame = "-" * 79
- message = _wrap_lines(message)
+ if wrap:
+ message = _wrap_lines(message)
self.outfile.write(
"{line}{frame}{line}{msg}{line}{frame}{line}".format(
line=os.linesep, frame=side_frame, msg=message))
if pause:
- six.moves.input("Press Enter to Continue")
+ if self._can_interact(force_interactive):
+ six.moves.input("Press Enter to Continue")
+ else:
+ logger.debug("Not pausing for user confirmation")
def menu(self, message, choices, ok_label="", cancel_label="",
- help_label="", **unused_kwargs):
+ help_label="", default=None,
+ cli_flag=None, force_interactive=False, **unused_kwargs):
# pylint: disable=unused-argument
"""Display a menu.
@@ -261,7 +100,10 @@ class FileDisplay(object):
:param choices: Menu lines, len must be > 0
:type choices: list of tuples (tag, item) or
list of descriptions (tags will be enumerated)
- :param dict _kwargs: absorbs default / cli_args
+ :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
+ because it won't cause any workflow regressions
:returns: tuple of (`code`, `index`) where
`code` - str display exit code
@@ -270,18 +112,25 @@ class FileDisplay(object):
:rtype: tuple
"""
+ if self._return_default(message, default, cli_flag, force_interactive):
+ return OK, default
+
self._print_menu(message, choices)
code, selection = self._get_valid_int_ans(len(choices))
return code, selection - 1
- def input(self, message, **unused_kwargs):
+ def input(self, message, default=None,
+ cli_flag=None, force_interactive=False, **unused_kwargs):
# pylint: disable=no-self-use
"""Accept input from the user.
:param str message: message to display to the user
- :param dict _kwargs: absorbs default / cli_args
+ :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
+ because it won't cause any workflow regressions
:returns: tuple of (`code`, `input`) where
`code` - str display exit code
@@ -289,6 +138,9 @@ class FileDisplay(object):
:rtype: tuple
"""
+ if self._return_default(message, default, cli_flag, force_interactive):
+ return OK, default
+
ans = six.moves.input(
textwrap.fill(
"%s (Enter 'c' to cancel): " % message,
@@ -301,7 +153,8 @@ class FileDisplay(object):
else:
return OK, ans
- def yesno(self, message, yes_label="Yes", no_label="No", **unused_kwargs):
+ def yesno(self, message, yes_label="Yes", no_label="No", default=None,
+ cli_flag=None, force_interactive=False, **unused_kwargs):
"""Query the user with a yes/no question.
Yes and No label must begin with different letters, and must contain at
@@ -310,12 +163,18 @@ class FileDisplay(object):
:param str message: question for the user
:param str yes_label: Label of the "Yes" parameter
:param str no_label: Label of the "No" parameter
- :param dict _kwargs: absorbs default / cli_args
+ :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
+ because it won't cause any workflow regressions
:returns: True for "Yes", False for "No"
:rtype: bool
"""
+ if self._return_default(message, default, cli_flag, force_interactive):
+ return default
+
side_frame = ("-" * 79) + os.linesep
message = _wrap_lines(message)
@@ -337,14 +196,18 @@ class FileDisplay(object):
ans.startswith(no_label[0].upper())):
return False
- def checklist(self, message, tags, default_status=True, **unused_kwargs):
+ def checklist(self, message, tags, default_status=True, 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 dict _kwargs: absorbs default / cli_args
+ :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
+ because it won't cause any workflow regressions
:returns: tuple of (`code`, `tags`) where
`code` - str display exit code
@@ -352,13 +215,20 @@ class FileDisplay(object):
:rtype: tuple
"""
+ if self._return_default(message, default, cli_flag, force_interactive):
+ return OK, default
+
while True:
self._print_menu(message, tags)
code, ans = self.input("Select the appropriate numbers separated "
- "by commas and/or spaces")
+ "by commas and/or spaces, or leave input "
+ "blank to select all options shown",
+ force_interactive=True)
if code == OK:
+ if len(ans.strip()) == 0:
+ ans = " ".join(str(x) for x in range(1, len(tags)+1))
indices = separate_list_input(ans)
selected_tags = self._scrub_checklist_input(indices, tags)
if selected_tags:
@@ -369,10 +239,67 @@ class FileDisplay(object):
else:
return code, []
- def directory_select(self, message, **unused_kwargs):
+ def _return_default(self, prompt, default, cli_flag, force_interactive):
+ """Should we return the default instead of prompting the user?
+
+ :param str prompt: prompt for the user
+ :param default: default answer to prompt
+ :param str cli_flag: command line option for setting an answer
+ to this question
+ :param bool force_interactive: if interactivity is forced by the
+ IDisplay call
+
+ :returns: True if we should return the default without prompting
+ :rtype: bool
+
+ """
+ # assert_valid_call(prompt, default, cli_flag, force_interactive)
+ if self._can_interact(force_interactive):
+ return False
+ elif default is None:
+ msg = "Unable to get an answer for the question:\n{0}".format(prompt)
+ if cli_flag:
+ msg += (
+ "\nYou can provide an answer on the "
+ "command line with the {0} flag.".format(cli_flag))
+ raise errors.Error(msg)
+ else:
+ logger.debug(
+ "Falling back to default %s for the prompt:\n%s",
+ default, prompt)
+ return True
+
+ def _can_interact(self, force_interactive):
+ """Can we safely interact with the user?
+
+ :param bool force_interactive: if interactivity is forced by the
+ IDisplay call
+
+ :returns: True if the display can interact with the user
+ :rtype: bool
+
+ """
+ if (self.force_interactive or force_interactive or
+ sys.stdin.isatty() and self.outfile.isatty()):
+ return True
+ elif not self.skipped_interaction:
+ logger.warning(
+ "Skipped user interaction because Certbot doesn't appear to "
+ "be running in a terminal. You should probably include "
+ "--non-interactive or %s on the command line.",
+ constants.FORCE_INTERACTIVE_FLAG)
+ self.skipped_interaction = True
+ return False
+
+ def directory_select(self, message, default=None, cli_flag=None,
+ force_interactive=False, **unused_kwargs):
"""Display a directory selection screen.
:param str message: prompt to give the user
+ :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
+ because it won't cause any workflow regressions
:returns: tuple of the form (`code`, `string`) where
`code` - display exit code
@@ -380,7 +307,7 @@ class FileDisplay(object):
"""
with completer.Completer():
- return self.input(message)
+ return self.input(message, default, cli_flag, force_interactive)
def _scrub_checklist_input(self, indices, tags):
# pylint: disable=no-self-use
@@ -478,11 +405,29 @@ class FileDisplay(object):
return OK, selection
+def assert_valid_call(prompt, default, cli_flag, force_interactive):
+ """Verify that provided arguments is a valid IDisplay call.
+
+ :param str prompt: prompt for the user
+ :param default: default answer to prompt
+ :param str cli_flag: command line option for setting an answer
+ to this question
+ :param bool force_interactive: if interactivity is forced by the
+ IDisplay call
+
+ """
+ msg = "Invalid IDisplay call for this prompt:\n{0}".format(prompt)
+ if cli_flag:
+ msg += ("\nYou can set an answer to "
+ "this prompt with the {0} flag".format(cli_flag))
+ assert default is not None or force_interactive, msg
+
+
@zope.interface.implementer(interfaces.IDisplay)
class NoninteractiveDisplay(object):
"""An iDisplay implementation that never asks for interactive user input"""
- def __init__(self, outfile):
+ def __init__(self, outfile, *unused_args, **unused_kwargs):
super(NoninteractiveDisplay, self).__init__()
self.outfile = outfile
@@ -496,23 +441,24 @@ class NoninteractiveDisplay(object):
msg += "\n\n(You can set this with the {0} flag)".format(cli_flag)
raise errors.MissingCommandlineFlag(msg)
- def notification(self, message, height=10, pause=False):
+ def notification(self, message, pause=False, wrap=True, **unused_kwargs):
# pylint: disable=unused-argument
"""Displays a notification without waiting for user acceptance.
:param str message: Message to display to stdout
- :param int height: No effect for NoninteractiveDisplay
:param bool pause: The NoninteractiveDisplay waits for no keyboard
+ :param bool wrap: Whether or not the application should wrap text
"""
side_frame = "-" * 79
- message = _wrap_lines(message)
+ if wrap:
+ message = _wrap_lines(message)
self.outfile.write(
"{line}{frame}{line}{msg}{line}{frame}{line}".format(
line=os.linesep, frame=side_frame, msg=message))
def menu(self, message, choices, ok_label=None, cancel_label=None,
- help_label=None, default=None, cli_flag=None):
+ help_label=None, default=None, cli_flag=None, **unused_kwargs):
# pylint: disable=unused-argument,too-many-arguments
"""Avoid displaying a menu.
@@ -535,7 +481,7 @@ class NoninteractiveDisplay(object):
return OK, default
- def input(self, message, default=None, cli_flag=None):
+ def input(self, message, default=None, cli_flag=None, **unused_kwargs):
"""Accept input from the user.
:param str message: message to display to the user
@@ -552,7 +498,8 @@ class NoninteractiveDisplay(object):
else:
return OK, default
- def yesno(self, message, yes_label=None, no_label=None, default=None, cli_flag=None):
+ def yesno(self, message, yes_label=None, no_label=None,
+ default=None, cli_flag=None, **unused_kwargs):
# pylint: disable=unused-argument
"""Decide Yes or No, without asking anybody
@@ -569,8 +516,8 @@ class NoninteractiveDisplay(object):
else:
return default
- def checklist(self, message, tags, default=None, cli_flag=None, **kwargs):
- # pylint: disable=unused-argument
+ def checklist(self, message, tags, default=None,
+ cli_flag=None, **unused_kwargs):
"""Display a checklist.
:param str message: Message to display to user
@@ -588,7 +535,8 @@ class NoninteractiveDisplay(object):
else:
return OK, default
- def directory_select(self, message, default=None, cli_flag=None):
+ def directory_select(self, message, default=None,
+ cli_flag=None, **unused_kwargs):
"""Simulate prompting the user for a directory.
This function returns default if it is not ``None``, otherwise,
diff --git a/certbot/hooks.py b/certbot/hooks.py
index f37f81c6e..5cda478cc 100644
--- a/certbot/hooks.py
+++ b/certbot/hooks.py
@@ -7,21 +7,35 @@ import os
from subprocess import Popen, PIPE
from certbot import errors
+from certbot import util
+
+from certbot.plugins import util as plug_util
logger = logging.getLogger(__name__)
def validate_hooks(config):
"""Check hook commands are executable."""
- _validate_hook(config.pre_hook, "pre")
- _validate_hook(config.post_hook, "post")
- _validate_hook(config.renew_hook, "renew")
+ validate_hook(config.pre_hook, "pre")
+ validate_hook(config.post_hook, "post")
+ validate_hook(config.renew_hook, "renew")
def _prog(shell_cmd):
- """Extract the program run by a shell command"""
- cmd = _which(shell_cmd)
- return os.path.basename(cmd) if cmd else None
+ """Extract the program run by a shell command.
-def _validate_hook(shell_cmd, hook_name):
+ :param str shell_cmd: command to be executed
+
+ :returns: basename of command or None if the command isn't found
+ :rtype: str or None
+
+ """
+ if not util.exe_exists(shell_cmd):
+ plug_util.path_surgery(shell_cmd)
+ if not util.exe_exists(shell_cmd):
+ return None
+ return os.path.basename(shell_cmd)
+
+
+def validate_hook(shell_cmd, hook_name):
"""Check that a command provided as a hook is plausibly executable.
:raises .errors.HookCommandNotFound: if the command is not found
@@ -36,68 +50,79 @@ def _validate_hook(shell_cmd, hook_name):
def pre_hook(config):
"Run pre-hook if it's defined and hasn't been run."
- if config.pre_hook and not pre_hook.already:
- logger.info("Running pre-hook command: %s", config.pre_hook)
- _run_hook(config.pre_hook)
- pre_hook.already = True
+ cmd = config.pre_hook
+ if cmd and cmd not in pre_hook.already:
+ logger.info("Running pre-hook command: %s", cmd)
+ _run_hook(cmd)
+ pre_hook.already.add(cmd)
+ elif cmd:
+ logger.info("Pre-hook command already run, skipping: %s", cmd)
-pre_hook.already = False
+pre_hook.already = set()
-def post_hook(config, final=False):
+
+def post_hook(config):
"""Run post hook if defined.
If the verb is renew, we might have more certs to renew, so we wait until
- we're called with final=True before actually doing anything.
+ run_saved_post_hooks() is called.
"""
- if config.post_hook:
- if not pre_hook.already:
- logger.info("No renewals attempted, so not running post-hook")
- if config.verb != "renew":
- logger.warning("Sanity failure in renewal hooks")
- return
- if final or config.verb != "renew":
- logger.info("Running post-hook command: %s", config.post_hook)
- _run_hook(config.post_hook)
+
+ cmd = config.post_hook
+ # In the "renew" case, we save these up to run at the end
+ if config.verb == "renew":
+ if cmd and cmd not in post_hook.eventually:
+ post_hook.eventually.append(cmd)
+ # certonly / run
+ elif cmd:
+ logger.info("Running post-hook command: %s", cmd)
+ _run_hook(cmd)
+
+post_hook.eventually = []
+
+def run_saved_post_hooks():
+ """Run any post hooks that were saved up in the course of the 'renew' verb"""
+ for cmd in post_hook.eventually:
+ logger.info("Running post-hook command: %s", cmd)
+ _run_hook(cmd)
+
def renew_hook(config, domains, lineage_path):
- "Run post-renewal hook if defined."
+ """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)
_run_hook(config.renew_hook)
else:
logger.warning("Dry run: skipping renewal hook command: %s", config.renew_hook)
+
def _run_hook(shell_cmd):
"""Run a hook command.
:returns: stderr if there was any"""
- cmd = Popen(shell_cmd, shell=True, stdout=PIPE, stderr=PIPE, stdin=PIPE)
- _out, err = cmd.communicate()
+ err, _ = execute(shell_cmd)
+ return err
+
+
+def execute(shell_cmd):
+ """Run a command.
+
+ :returns: `tuple` (`str` stderr, `str` stdout)"""
+
+ # universal_newlines causes Popen.communicate()
+ # to return str objects instead of bytes in Python 3
+ cmd = Popen(shell_cmd, shell=True, stdout=PIPE,
+ stderr=PIPE, universal_newlines=True)
+ out, err = cmd.communicate()
if cmd.returncode != 0:
- logger.error('Hook command "%s" returned error code %d', shell_cmd, cmd.returncode)
+ logger.error('Hook command "%s" returned error code %d',
+ shell_cmd, cmd.returncode)
if err:
- logger.error('Error output from %s:\n%s', _prog(shell_cmd), err)
+ base_cmd = os.path.basename(shell_cmd.split(None, 1)[0])
+ logger.error('Error output from %s:\n%s', base_cmd, err)
+ return (err, out)
-def _is_exe(fpath):
- return os.path.isfile(fpath) and os.access(fpath, os.X_OK)
-
-def _which(program):
- """Test if program is in the path."""
- # Borrowed from:
- # https://stackoverflow.com/questions/377017/test-if-executable-exists-in-python
- # XXX May need more porting to handle .exe extensions on Windows
-
- fpath, _fname = os.path.split(program)
- if fpath:
- if _is_exe(program):
- return program
- else:
- for path in os.environ["PATH"].split(os.pathsep):
- exe_file = os.path.join(path, program)
- if _is_exe(exe_file):
- return exe_file
-
- return None
diff --git a/certbot/interfaces.py b/certbot/interfaces.py
index 42a952f10..2df2abfe8 100644
--- a/certbot/interfaces.py
+++ b/certbot/interfaces.py
@@ -138,15 +138,15 @@ class IAuthenticator(IPlugin):
"""
def get_chall_pref(domain):
- """Return list of challenge preferences.
+ """Return `collections.Iterable` of challenge preferences.
:param str domain: Domain for which challenge preferences are sought.
- :returns: List of challenge types (subclasses of
+ :returns: `collections.Iterable` of challenge types (subclasses of
:class:`acme.challenges.Challenge`) with the most
preferred challenges first. If a type is not specified, it means the
Authenticator cannot perform the challenge.
- :rtype: list
+ :rtype: `collections.Iterable`
"""
@@ -158,7 +158,7 @@ class IAuthenticator(IPlugin):
instances, such that it contains types found within
:func:`get_chall_pref` only.
- :returns: List of ACME
+ :returns: `collections.Iterable` of ACME
:class:`~acme.challenges.ChallengeResponse` instances
or if the :class:`~acme.challenges.Challenge` cannot
be fulfilled then:
@@ -168,7 +168,7 @@ class IAuthenticator(IPlugin):
``False``
Authenticator will never be able to perform (error).
- :rtype: :class:`list` of
+ :rtype: :class:`collections.Iterable` of
:class:`acme.challenges.ChallengeResponse`,
where responses are required to be returned in
the same order as corresponding input challenges
@@ -201,7 +201,7 @@ class IConfig(zope.interface.Interface):
"""
server = zope.interface.Attribute("ACME Directory Resource URI.")
email = zope.interface.Attribute(
- "Email used for registration and recovery contact.")
+ "Email used for registration and recovery contact. (default: Ask)")
rsa_key_size = zope.interface.Attribute("Size of the RSA key.")
must_staple = zope.interface.Attribute(
"Adds the OCSP Must Staple extension to the certificate. "
@@ -223,9 +223,6 @@ class IConfig(zope.interface.Interface):
temp_checkpoint_dir = zope.interface.Attribute(
"Temporary checkpoint directory.")
- renewer_config_file = zope.interface.Attribute(
- "Location of renewal configuration file.")
-
no_verify_ssl = zope.interface.Attribute(
"Disable verification of the ACME server's certificate.")
tls_sni_01_port = zope.interface.Attribute(
@@ -234,7 +231,7 @@ class IConfig(zope.interface.Interface):
"A conforming ACME server will still attempt to connect on port 443.")
http01_port = zope.interface.Attribute(
- "Port used in the http-01 challenge."
+ "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.")
@@ -257,7 +254,7 @@ class IInstaller(IPlugin):
def get_all_names():
"""Returns all names that may be authenticated.
- :rtype: `list` of `str`
+ :rtype: `collections.Iterable` of `str`
"""
@@ -292,24 +289,11 @@ class IInstaller(IPlugin):
"""
def supported_enhancements():
- """Returns a list of supported enhancements.
+ """Returns a `collections.Iterable` of supported enhancements.
:returns: supported enhancements which should be a subset of
:const:`~certbot.constants.ENHANCEMENTS`
- :rtype: :class:`list` of :class:`str`
-
- """
-
- def get_all_certs_keys():
- """Retrieve all certs and keys set in configuration.
-
- :returns: tuples with form `[(cert, key, path)]`, where:
-
- - `cert` - str path to certificate file
- - `key` - str path to associated key file
- - `path` - file path to configuration file
-
- :rtype: list
+ :rtype: :class:`collections.Iterable` of :class:`str`
"""
@@ -377,21 +361,29 @@ class IInstaller(IPlugin):
class IDisplay(zope.interface.Interface):
"""Generic display."""
+ # pylint: disable=too-many-arguments
+ # see https://github.com/certbot/certbot/issues/3915
- def notification(message, height, pause):
+ def notification(message, pause, wrap=True, force_interactive=False):
"""Displays a string message
:param str message: Message to display
- :param int height: Height of dialog box if applicable
:param bool pause: Whether or not the application should pause for
confirmation (if available)
+ :param bool wrap: Whether or not the application should wrap text
+ :param bool force_interactive: True if it's safe to prompt the user
+ because it won't cause any workflow regressions
"""
- def menu(message, choices, ok_label="OK", # pylint: disable=too-many-arguments
- cancel_label="Cancel", help_label="", default=None, cli_flag=None):
+ def menu(message, choices, ok_label="OK",
+ cancel_label="Cancel", help_label="",
+ default=None, cli_flag=None, force_interactive=False):
"""Displays a generic menu.
+ When not setting force_interactive=True, you must provide a
+ default value.
+
:param str message: message to display
:param choices: choices
@@ -402,6 +394,8 @@ class IDisplay(zope.interface.Interface):
:param str help_label: label for Help button
: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
+ because it won't cause any workflow regressions
:returns: tuple of (`code`, `index`) where
`code` - str display exit code
@@ -412,10 +406,16 @@ class IDisplay(zope.interface.Interface):
"""
- def input(message, default=None, cli_args=None):
+ def input(message, default=None, cli_args=None, force_interactive=False):
"""Accept input from the user.
+ When not setting force_interactive=True, you must provide a
+ default value.
+
:param str message: message to display to the user
+ :param str default: default (non-interactive) response to prompt
+ :param bool force_interactive: True if it's safe to prompt the user
+ because it won't cause any workflow regressions
:returns: tuple of (`code`, `input`) where
`code` - str display exit code
@@ -428,14 +428,19 @@ class IDisplay(zope.interface.Interface):
"""
def yesno(message, yes_label="Yes", no_label="No", default=None,
- cli_args=None):
+ cli_args=None, force_interactive=False):
"""Query the user with a yes/no question.
Yes and No label must begin with different letters.
+ When not setting force_interactive=True, you must provide a
+ default value.
+
:param str message: question for the user
:param str default: default (non-interactive) choice from the menu
:param str cli_flag: to automate choice from the menu, eg "--redirect / --no-redirect"
+ :param bool force_interactive: True if it's safe to prompt the user
+ because it won't cause any workflow regressions
:returns: True for "Yes", False for "No"
:rtype: bool
@@ -445,14 +450,20 @@ class IDisplay(zope.interface.Interface):
"""
- def checklist(message, tags, default_state, default=None, cli_args=None):
+ def checklist(message, tags, default_state,
+ 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
+ default value.
+
: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
+ because it won't cause any workflow regressions
:returns: tuple of the form (code, list_tags) where
`code` - int display exit code
@@ -464,15 +475,21 @@ class IDisplay(zope.interface.Interface):
"""
- def directory_select(self, message, default=None, cli_flag=None):
+ def directory_select(self, message, default=None,
+ cli_flag=None, force_interactive=False):
"""Display a directory selection screen.
+ When not setting force_interactive=True, you must provide a
+ default value.
+
:param str message: prompt to give the user
:param default: the default value to return, if one exists, when
using the NoninteractiveDisplay
:param str cli_flag: option used to set this value with the CLI,
if one exists, to be included in error messages given by
NoninteractiveDisplay
+ :param bool force_interactive: True if it's safe to prompt the user
+ because it won't cause any workflow regressions
:returns: tuple of the form (`code`, `string`) where
`code` - int display exit code
diff --git a/certbot/log.py b/certbot/log.py
deleted file mode 100644
index 62241254a..000000000
--- a/certbot/log.py
+++ /dev/null
@@ -1,64 +0,0 @@
-"""Logging utilities."""
-import logging
-
-import dialog
-
-from certbot.display import util as display_util
-
-
-class DialogHandler(logging.Handler): # pylint: disable=too-few-public-methods
- """Logging handler using dialog info box.
-
- :ivar int height: Height of the info box (without padding).
- :ivar int width: Width of the info box (without padding).
- :ivar list lines: Lines to be displayed in the info box.
- :ivar d: Instance of :class:`dialog.Dialog`.
-
- """
-
- PADDING_HEIGHT = 2
- PADDING_WIDTH = 4
-
- def __init__(self, level=logging.NOTSET, height=display_util.HEIGHT,
- width=display_util.WIDTH - 4, d=None):
- # Handler not new-style -> no super
- logging.Handler.__init__(self, level)
- self.height = height
- self.width = width
- # "dialog" collides with module name...
- self.d = dialog.Dialog() if d is None else d
- self.lines = []
-
- def emit(self, record):
- """Emit message to a dialog info box.
-
- Only show the last (self.height) lines; note that lines can wrap
- at self.width, so a single line could actually be multiple
- lines.
-
- """
- for line in self.format(record).splitlines():
- # check for lines that would wrap
- cur_out = line
- while len(cur_out) > self.width:
- # find first space before self.width chars into cur_out
- last_space_pos = cur_out.rfind(' ', 0, self.width)
-
- if last_space_pos == -1:
- # no spaces, just cut them off at whatever
- self.lines.append(cur_out[0:self.width])
- cur_out = cur_out[self.width:]
- else:
- # cut off at last space
- self.lines.append(cur_out[0:last_space_pos])
- cur_out = cur_out[last_space_pos + 1:]
- if cur_out != '':
- self.lines.append(cur_out)
-
- # show last 16 lines
- content = '\n'.join(self.lines[-self.height:])
-
- # add the padding around the box
- self.d.infobox(
- content, self.height + self.PADDING_HEIGHT,
- self.width + self.PADDING_WIDTH)
diff --git a/certbot/main.py b/certbot/main.py
index 5c8105ddd..ec901c501 100644
--- a/certbot/main.py
+++ b/certbot/main.py
@@ -1,7 +1,6 @@
"""Certbot main entry point."""
from __future__ import print_function
import atexit
-import dialog
import functools
import logging.handlers
import os
@@ -13,10 +12,12 @@ import zope.component
from acme import jose
from acme import messages
+from acme import errors as acme_errors
import certbot
from certbot import account
+from certbot import cert_manager
from certbot import client
from certbot import cli
from certbot import crypto_util
@@ -27,10 +28,8 @@ from certbot import errors
from certbot import hooks
from certbot import interfaces
from certbot import util
-from certbot import log
from certbot import reporter
from certbot import renewal
-from certbot import storage
from certbot.display import util as display_util, ops as display_ops
from certbot.plugins import disco as plugins_disco
@@ -40,7 +39,10 @@ from certbot.plugins import selection as plug_sel
_PERM_ERR_FMT = os.linesep.join((
"The following error was encountered:", "{0}",
"If running as non-root, set --config-dir, "
- "--logs-dir, and --work-dir to writeable paths."))
+ "--work-dir, and --logs-dir to writeable paths."))
+
+USER_CANCELLED = ("User chose to cancel the operation and may "
+ "reinvoke the client.")
logger = logging.getLogger(__name__)
@@ -68,17 +70,21 @@ def _report_successful_dry_run(config):
reporter_util.HIGH_PRIORITY, on_crash=False)
-def _auth_from_domains(le_client, config, domains, lineage=None):
+def _auth_from_available(le_client, config, domains=None, certname=None, lineage=None):
"""Authenticate and enroll certificate.
- :returns: Tuple of (str action, cert_or_None) as per _treat_as_renewal
+ This method finds the relevant lineage, figures out what to do with it,
+ then performs that action. Includes calls to hooks, various reports,
+ checks, and requests for user input.
+
+ :returns: Tuple of (str action, cert_or_None) as per _find_lineage_for_domains_and_certname
action can be: "newcert" | "renew" | "reinstall"
"""
# If lineage is specified, use that one instead of looking around for
# a matching one.
if lineage is None:
# This will find a relevant matching lineage that exists
- action, lineage = _treat_as_renewal(config, domains)
+ action, lineage = _find_lineage_for_domains_and_certname(config, domains, certname)
else:
# Renewal, where we already know the specific lineage we're
# interested in
@@ -94,15 +100,15 @@ def _auth_from_domains(le_client, config, domains, lineage=None):
try:
if action == "renew":
logger.info("Renewing an existing certificate")
- renewal.renew_cert(config, domains, le_client, lineage)
+ renewal.renew_cert(config, le_client, lineage)
elif action == "newcert":
# TREAT AS NEW REQUEST
logger.info("Obtaining a new certificate")
- lineage = le_client.obtain_and_enroll_certificate(domains)
+ lineage = le_client.obtain_and_enroll_certificate(domains, certname)
if lineage is False:
raise errors.Error("Certificate could not be obtained")
finally:
- hooks.post_hook(config, final=False)
+ hooks.post_hook(config)
if not config.dry_run and not config.verb == "renew":
_report_new_cert(config, lineage.cert, lineage.fullchain)
@@ -115,7 +121,7 @@ def _handle_subset_cert_request(config, domains, cert):
:param storage.RenewableCert cert:
- :returns: Tuple of (str action, cert_or_None) as per _treat_as_renewal
+ :returns: Tuple of (str action, cert_or_None) as per _find_lineage_for_domains_and_certname
action can be: "newcert" | "renew" | "reinstall"
:rtype: tuple
@@ -133,7 +139,8 @@ def _handle_subset_cert_request(config, domains, cert):
br=os.linesep)
if config.expand or config.renew_by_default or zope.component.getUtility(
interfaces.IDisplay).yesno(question, "Expand", "Cancel",
- cli_flag="--expand"):
+ cli_flag="--expand",
+ force_interactive=True):
return "renew", cert
else:
reporter_util = zope.component.getUtility(interfaces.IReporter)
@@ -147,9 +154,7 @@ def _handle_subset_cert_request(config, domains, cert):
br=os.linesep
),
reporter_util.HIGH_PRIORITY)
- raise errors.Error(
- "User chose to cancel the operation and may "
- "reinvoke the client.")
+ raise errors.Error(USER_CANCELLED)
def _handle_identical_cert_request(config, lineage):
@@ -157,7 +162,7 @@ def _handle_identical_cert_request(config, lineage):
:param storage.RenewableCert lineage:
- :returns: Tuple of (str action, cert_or_None) as per _treat_as_renewal
+ :returns: Tuple of (str action, cert_or_None) as per _find_lineage_for_domains_and_certname
action can be: "newcert" | "renew" | "reinstall"
:rtype: tuple
@@ -171,8 +176,8 @@ def _handle_identical_cert_request(config, lineage):
# reinstalled without further prompting.
return "reinstall", lineage
question = (
- "You have an existing certificate that contains exactly the same "
- "domains you requested and isn't close to expiry."
+ "You have an existing certificate that has exactly the same "
+ "domains or certificate name you requested and isn't close to expiry."
"{br}(ref: {0}){br}{br}What would you like to do?"
).format(lineage.configfile.filename, br=os.linesep)
@@ -184,7 +189,8 @@ 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", default=0)
+ response = display.menu(question, choices, "OK", "Cancel",
+ default=0, force_interactive=True)
if response[0] == display_util.CANCEL:
# TODO: Add notification related to command-line options for
# skipping the menu for this case.
@@ -198,8 +204,7 @@ def _handle_identical_cert_request(config, lineage):
else:
assert False, "This is impossible"
-
-def _treat_as_renewal(config, domains):
+def _find_lineage_for_domains(config, domains):
"""Determine whether there are duplicated names and how to handle
them (renew, reinstall, newcert, or raising an error to stop
the client run if the user chooses to cancel the operation when
@@ -219,7 +224,7 @@ def _treat_as_renewal(config, domains):
if config.duplicate:
return "newcert", None
# TODO: Also address superset case
- ident_names_cert, subset_names_cert = _find_duplicative_certs(config, domains)
+ ident_names_cert, subset_names_cert = cert_manager.find_duplicative_certs(config, domains)
# XXX ^ schoen is not sure whether that correctly reads the systemwide
# configuration file.
if ident_names_cert is None and subset_names_cert is None:
@@ -230,51 +235,75 @@ def _treat_as_renewal(config, domains):
elif subset_names_cert is not None:
return _handle_subset_cert_request(config, domains, subset_names_cert)
+def _find_lineage_for_domains_and_certname(config, domains, certname):
+ """Find appropriate lineage based on given domains and/or certname.
-def _find_duplicative_certs(config, domains):
- """Find existing certs that duplicate the request."""
+ :returns: Two-element tuple containing desired new-certificate behavior as
+ a string token ("reinstall", "renew", or "newcert"), plus either
+ a RenewableCert instance or None if renewal shouldn't occur.
- identical_names_cert, subset_names_cert = None, None
+ :raises .Error: If the user would like to rerun the client again.
- cli_config = configuration.RenewerConfiguration(config)
- configs_dir = cli_config.renewal_configs_dir
- # Verify the directory is there
- util.make_or_verify_dir(configs_dir, mode=0o755, uid=os.geteuid())
+ """
+ if not certname:
+ return _find_lineage_for_domains(config, domains)
+ else:
+ lineage = cert_manager.lineage_for_certname(config, certname)
+ if lineage:
+ if domains:
+ if set(cert_manager.domains_for_certname(config, certname)) != set(domains):
+ _ask_user_to_confirm_new_names(config, domains, certname,
+ lineage.names()) # raises if no
+ return "renew", lineage
+ # unnecessarily specified domains or no domains specified
+ return _handle_identical_cert_request(config, lineage)
+ else:
+ if domains:
+ return "newcert", None
+ else:
+ raise errors.ConfigurationError("No certificate with name {0} found. "
+ "Use -d to specify domains, or run certbot --certificates to see "
+ "possible certificate names.".format(certname))
- for renewal_file in renewal.renewal_conf_files(cli_config):
- try:
- candidate_lineage = storage.RenewableCert(renewal_file, cli_config)
- except (errors.CertStorageError, IOError):
- logger.warning("Renewal conf file %s is broken. Skipping.", renewal_file)
- logger.debug("Traceback was:\n%s", traceback.format_exc())
- continue
- # TODO: Handle these differently depending on whether they are
- # expired or still valid?
- candidate_names = set(candidate_lineage.names())
- if candidate_names == set(domains):
- identical_names_cert = candidate_lineage
- elif candidate_names.issubset(set(domains)):
- # This logic finds and returns the largest subset-names cert
- # in the case where there are several available.
- if subset_names_cert is None:
- subset_names_cert = candidate_lineage
- elif len(candidate_names) > len(subset_names_cert.names()):
- subset_names_cert = candidate_lineage
+def _ask_user_to_confirm_new_names(config, new_domains, certname, old_domains):
+ """Ask user to confirm update cert certname to contain new_domains.
+ """
+ if config.renew_with_new_domains:
+ return
+ msg = ("Confirm that you intend to update certificate {0} "
+ "to include domains {1}. Note that it previously "
+ "contained domains {2}.".format(
+ certname,
+ new_domains,
+ old_domains))
+ obj = zope.component.getUtility(interfaces.IDisplay)
+ if not obj.yesno(msg, "Update cert", "Cancel", default=True):
+ raise errors.ConfigurationError("Specified mismatched cert name and domains.")
- return identical_names_cert, subset_names_cert
-
-
-def _find_domains(config, installer):
+def _find_domains_or_certname(config, installer):
+ """Retrieve domains and certname from config or user input.
+ """
+ domains = None
+ certname = config.certname
+ # first, try to get domains from the config
if config.domains:
domains = config.domains
- else:
+ # if we can't do that but we have a certname, get the domains
+ # with that certname
+ elif certname:
+ domains = cert_manager.domains_for_certname(config, certname)
+
+ # that certname might not have existed, or there was a problem.
+ # try to get domains from the user.
+ if not domains:
domains = display_ops.choose_names(installer)
- if not domains:
+ if not domains and not certname:
raise errors.Error("Please specify --domains, or --installer that "
- "will help in domain names autodiscovery")
+ "will help in domain names autodiscovery, or "
+ "--cert-name for an existing certificate name.")
- return domains
+ return domains, certname
def _report_new_cert(config, cert_path, fullchain_path):
@@ -347,7 +376,8 @@ def _determine_account(config):
"server at {1}".format(
regr.terms_of_service, config.server))
obj = zope.component.getUtility(interfaces.IDisplay)
- return obj.yesno(msg, "Agree", "Cancel", cli_flag="--agree-tos")
+ return obj.yesno(msg, "Agree", "Cancel",
+ cli_flag="--agree-tos", force_interactive=True)
try:
acc, acme = client.register(
@@ -429,13 +459,13 @@ def install(config, plugins):
except errors.PluginSelectionError as e:
return e.message
- domains = _find_domains(config, installer)
+ domains, _ = _find_domains_or_certname(config, installer)
le_client = _init_le_client(config, authenticator=None, installer=installer)
assert config.cert_path is not None # required=True in the subparser
le_client.deploy_certificate(
domains, config.key_path, config.cert_path, config.chain_path,
config.fullchain_path)
- le_client.enhance_config(domains, config, config.chain_path)
+ le_client.enhance_config(domains, config.chain_path)
def plugins_cmd(config, plugins): # TODO: Use IDisplay rather than print
@@ -477,6 +507,34 @@ def config_changes(config, unused_plugins):
"""
client.view_config_changes(config, num=config.num)
+def update_symlinks(config, unused_plugins):
+ """Update the certificate file family symlinks
+
+ Use the information in the config file to make symlinks point to
+ the correct archive directory.
+ """
+ cert_manager.update_live_symlinks(config)
+
+def rename(config, unused_plugins):
+ """Rename a certificate
+
+ Use the information in the config file to rename an existing
+ lineage.
+ """
+ cert_manager.rename_lineage(config)
+
+def delete(config, unused_plugins):
+ """Delete a certificate
+
+ Use the information in the config file to delete an existing
+ lineage.
+ """
+ cert_manager.delete(config)
+
+def certificates(config, unused_plugins):
+ """Display information about certs configured with Certbot
+ """
+ cert_manager.certificates(config)
def revoke(config, unused_plugins): # TODO: coop with renewal config
"""Revoke a previously obtained certificate."""
@@ -492,7 +550,14 @@ def revoke(config, unused_plugins): # TODO: coop with renewal config
key = acc.key
acme = client.acme_from_config_key(config, key)
cert = crypto_util.pyopenssl_load_certificate(config.cert_path[1])[0]
- acme.revoke(jose.ComparableX509(cert))
+ logger.debug("Reason code for revocation: %s", config.reason)
+
+ try:
+ acme.revoke(jose.ComparableX509(cert), config.reason)
+ except acme_errors.ClientError as e:
+ return e.message
+
+ display_ops.success_revocation(config.cert_path[0])
def run(config, plugins): # pylint: disable=too-many-branches,too-many-locals
@@ -504,23 +569,23 @@ def run(config, plugins): # pylint: disable=too-many-branches,too-many-locals
except errors.PluginSelectionError as e:
return e.message
- domains = _find_domains(config, installer)
+ domains, certname = _find_domains_or_certname(config, installer)
# TODO: Handle errors from _init_le_client?
le_client = _init_le_client(config, authenticator, installer)
- action, lineage = _auth_from_domains(le_client, config, domains)
+ action, lineage = _auth_from_available(le_client, config, domains, certname)
le_client.deploy_certificate(
domains, lineage.privkey, lineage.cert,
lineage.chain, lineage.fullchain)
- le_client.enhance_config(domains, config, lineage.chain)
+ le_client.enhance_config(domains, lineage.chain)
- if len(lineage.available_versions("cert")) == 1:
+ if action in ("newcert", "reinstall",):
display_ops.success_installation(domains)
else:
- display_ops.success_renewal(domains, action)
+ display_ops.success_renewal(domains)
_suggest_donation_if_appropriate(config, action)
@@ -542,7 +607,6 @@ def _csr_obtain_cert(config, le_client):
certr, chain, config.cert_path, config.chain_path, config.fullchain_path)
_report_new_cert(config, cert_path, cert_fullchain)
-
def obtain_cert(config, plugins, lineage=None):
"""Authenticate & obtain cert, but do not install it.
@@ -560,8 +624,8 @@ def obtain_cert(config, plugins, lineage=None):
# SHOWTIME: Possibly obtain/renew a cert, and set action to renew | newcert | reinstall
if config.csr is None: # the common case
- domains = _find_domains(config, installer)
- action, _ = _auth_from_domains(le_client, config, domains, lineage)
+ domains, certname = _find_domains_or_certname(config, installer)
+ action, _ = _auth_from_available(le_client, config, domains, certname, lineage)
else:
assert lineage is None, "Did not expect a CSR with a RenewableCert"
_csr_obtain_cert(config, le_client)
@@ -590,9 +654,9 @@ def obtain_cert(config, plugins, lineage=None):
def renew(config, unused_plugins):
"""Renew previously-obtained certificates."""
try:
- renewal.renew_all_lineages(config)
+ renewal.handle_renewal_request(config)
finally:
- hooks.post_hook(config, final=True)
+ hooks.run_saved_post_hooks()
def setup_log_file_handler(config, logfile, fmt):
@@ -614,14 +678,9 @@ def setup_log_file_handler(config, logfile, fmt):
return handler, log_file_path
-def _cli_log_handler(config, level, fmt):
- if config.text_mode or config.noninteractive_mode or config.verb == "renew":
- handler = colored_logging.StreamHandler()
- handler.setFormatter(logging.Formatter(fmt))
- else:
- handler = log.DialogHandler()
- # dialog box is small, display as less as possible
- handler.setFormatter(logging.Formatter("%(message)s"))
+def _cli_log_handler(level, fmt):
+ handler = colored_logging.StreamHandler()
+ handler.setFormatter(logging.Formatter(fmt))
handler.setLevel(level)
return handler
@@ -641,7 +700,7 @@ def setup_logging(config):
level = -config.verbose_count * 10
file_handler, log_file_path = setup_log_file_handler(
config, logfile=logfile, fmt=file_fmt)
- cli_handler = _cli_log_handler(config, level, cli_fmt)
+ cli_handler = _cli_log_handler(level, cli_fmt)
# TODO: use fileConfig?
@@ -665,10 +724,8 @@ def _handle_exception(exc_type, exc_value, trace, config):
to the user. sys.exit is always called with a nonzero status.
"""
- logger.debug(
- "Exiting abnormally:%s%s",
- os.linesep,
- "".join(traceback.format_exception(exc_type, exc_value, trace)))
+ tb_str = "".join(traceback.format_exception(exc_type, exc_value, trace))
+ logger.debug("Exiting abnormally:%s%s", os.linesep, tb_str)
if issubclass(exc_type, Exception) and (config is None or not config.debug):
if config is None:
@@ -677,9 +734,11 @@ def _handle_exception(exc_type, exc_value, trace, config):
with open(logfile, "w") as logfd:
traceback.print_exception(
exc_type, exc_value, trace, file=logfd)
+ assert "--debug" not in sys.argv # config is None if this explodes
except: # pylint: disable=bare-except
- sys.exit("".join(
- traceback.format_exception(exc_type, exc_value, trace)))
+ sys.exit(tb_str)
+ if "--debug" in sys.argv:
+ sys.exit(tb_str)
if issubclass(exc_type, errors.Error):
sys.exit(exc_value)
@@ -687,10 +746,7 @@ def _handle_exception(exc_type, exc_value, trace, config):
# Here we're passing a client or ACME error out to the client at the shell
# Tell the user a bit about what happened, without overwhelming
# them with a full traceback
- if issubclass(exc_type, dialog.error):
- err = exc_value.complete_message()
- else:
- err = traceback.format_exception_only(exc_type, exc_value)[0]
+ err = traceback.format_exception_only(exc_type, exc_value)[0]
# Typical error from the ACME module:
# acme.messages.Error: urn:ietf:params:acme:error:malformed :: The
# request message was malformed :: Error creating new registration
@@ -707,8 +763,7 @@ def _handle_exception(exc_type, exc_value, trace, config):
msg += "logfiles in {0} for more details.".format(config.logs_dir)
sys.exit(msg)
else:
- sys.exit("".join(
- traceback.format_exception(exc_type, exc_value, trace)))
+ sys.exit(tb_str)
def make_or_verify_core_dir(directory, mode, uid, strict):
@@ -727,6 +782,44 @@ def make_or_verify_core_dir(directory, mode, uid, strict):
except OSError as error:
raise errors.Error(_PERM_ERR_FMT.format(error))
+def make_or_verify_needed_dirs(config):
+ """Create or verify existance of config, work, or logs directories"""
+ make_or_verify_core_dir(config.config_dir, constants.CONFIG_DIRS_MODE,
+ os.geteuid(), config.strict_permissions)
+ make_or_verify_core_dir(config.work_dir, constants.CONFIG_DIRS_MODE,
+ os.geteuid(), config.strict_permissions)
+ # TODO: logs might contain sensitive data such as contents of the
+ # private key! #525
+ make_or_verify_core_dir(config.logs_dir, 0o700,
+ os.geteuid(), config.strict_permissions)
+
+
+def set_displayer(config):
+ """Set the displayer"""
+ if config.quiet:
+ config.noninteractive_mode = True
+ displayer = display_util.NoninteractiveDisplay(open(os.devnull, "w"))
+ elif config.noninteractive_mode:
+ displayer = display_util.NoninteractiveDisplay(sys.stdout)
+ else:
+ displayer = display_util.FileDisplay(sys.stdout,
+ config.force_interactive)
+ zope.component.provideUtility(displayer)
+
+def _post_logging_setup(config, plugins, cli_args):
+ """Perform any setup or configuration tasks that require a logger."""
+
+ # This needs logging, but would otherwise be in HelpfulArgumentParser
+ if config.validate_hooks:
+ hooks.validate_hooks(config)
+
+ cli.possible_deprecation_warning(config)
+
+ logger.debug("certbot version: %s", certbot.__version__)
+ # do not log `config`, as it contains sensitive data (e.g. revoke --key)!
+ logger.debug("Arguments: %r", cli_args)
+ logger.debug("Discovered plugins: %r", plugins)
+
def main(cli_args=sys.argv[1:]):
"""Command line argument parsing and main script execution."""
@@ -738,37 +831,17 @@ def main(cli_args=sys.argv[1:]):
config = configuration.NamespaceConfig(args)
zope.component.provideUtility(config)
- make_or_verify_core_dir(config.config_dir, constants.CONFIG_DIRS_MODE,
- os.geteuid(), config.strict_permissions)
- make_or_verify_core_dir(config.work_dir, constants.CONFIG_DIRS_MODE,
- os.geteuid(), config.strict_permissions)
- # TODO: logs might contain sensitive data such as contents of the
- # private key! #525
- make_or_verify_core_dir(config.logs_dir, 0o700,
- os.geteuid(), config.strict_permissions)
+ make_or_verify_needed_dirs(config)
+
# Setup logging ASAP, otherwise "No handlers could be found for
# logger ..." TODO: this should be done before plugins discovery
setup_logging(config)
- cli.possible_deprecation_warning(config)
- logger.debug("certbot version: %s", certbot.__version__)
- # do not log `config`, as it contains sensitive data (e.g. revoke --key)!
- logger.debug("Arguments: %r", cli_args)
- logger.debug("Discovered plugins: %r", plugins)
+ _post_logging_setup(config, plugins, cli_args)
sys.excepthook = functools.partial(_handle_exception, config=config)
- # Displayer
- if config.quiet:
- config.noninteractive_mode = True
- displayer = display_util.NoninteractiveDisplay(open(os.devnull, "w"))
- elif config.noninteractive_mode:
- displayer = display_util.NoninteractiveDisplay(sys.stdout)
- elif config.text_mode:
- displayer = display_util.FileDisplay(sys.stdout)
- else:
- displayer = display_util.NcursesDisplay()
- zope.component.provideUtility(displayer)
+ set_displayer(config)
# Reporter
report = reporter.Reporter(config)
diff --git a/certbot/ocsp.py b/certbot/ocsp.py
new file mode 100644
index 000000000..8921dbb88
--- /dev/null
+++ b/certbot/ocsp.py
@@ -0,0 +1,120 @@
+"""Tools for checking certificate revocation."""
+import logging
+import re
+
+from subprocess import Popen, PIPE
+
+from certbot import errors
+from certbot import util
+
+logger = logging.getLogger(__name__)
+
+class RevocationChecker(object):
+ "This class figures out OCSP checking on this system, and performs it."
+
+ def __init__(self):
+ self.broken = False
+
+ if not util.exe_exists("openssl"):
+ logging.info("openssl not installed, can't check revocation")
+ self.broken = True
+ return
+
+ # New versions of openssl want -header var=val, old ones want -header var val
+ test_host_format = Popen(["openssl", "ocsp", "-header", "var", "val"],
+ stdout=PIPE, stderr=PIPE, universal_newlines=True)
+ _out, err = test_host_format.communicate()
+ if "Missing =" in err:
+ self.host_args = lambda host: ["Host=" + host]
+ else:
+ self.host_args = lambda host: ["Host", host]
+
+
+ def ocsp_revoked(self, cert_path, chain_path):
+ """Get revoked status for a particular cert version.
+
+ .. todo:: Make this a non-blocking call
+
+ :param str cert_path: Path to certificate
+ :param str chain_path: Path to intermediate cert
+ :rtype bool or None:
+ :returns: True if revoked; False if valid or the check failed
+
+ """
+ if self.broken:
+ return False
+
+
+ url, host = self.determine_ocsp_server(cert_path)
+ if not host:
+ return False
+ # jdkasten thanks "Bulletproof SSL and TLS - Ivan Ristic" for documenting this!
+ cmd = ["openssl", "ocsp",
+ "-no_nonce",
+ "-issuer", chain_path,
+ "-cert", cert_path,
+ "-url", url,
+ "-CAfile", chain_path,
+ "-verify_other", chain_path,
+ "-trust_other",
+ "-header"] + self.host_args(host)
+ logger.debug("Querying OCSP for %s", cert_path)
+ logger.debug(" ".join(cmd))
+ try:
+ output, err = util.run_script(cmd, log=logging.debug)
+ except errors.SubprocessError:
+ logger.info("OCSP check failed for %s (are we offline?)", cert_path)
+ return False
+
+ return _translate_ocsp_query(cert_path, output, err)
+
+
+ def determine_ocsp_server(self, cert_path):
+ """Extract the OCSP server host from a certificate.
+
+ :param str cert_path: Path to the cert we're checking OCSP for
+ :rtype tuple:
+ :returns: (OCSP server URL or None, OCSP server host or None)
+
+ """
+ try:
+ url, _err = util.run_script(
+ ["openssl", "x509", "-in", cert_path, "-noout", "-ocsp_uri"],
+ log=logging.debug)
+ except errors.SubprocessError:
+ logger.info("Cannot extract OCSP URI from %s", cert_path)
+ return None, None
+
+ url = url.rstrip()
+ host = url.partition("://")[2].rstrip("/")
+ if host:
+ return url, host
+ else:
+ logger.info("Cannot process OCSP host from URL (%s) in cert at %s", url, cert_path)
+ return None, None
+
+def _translate_ocsp_query(cert_path, ocsp_output, ocsp_errors):
+ """Parse openssl's weird output to work out what it means."""
+
+ states = ("good", "revoked", "unknown")
+ patterns = [r"{0}: (WARNING.*)?{1}".format(cert_path, s) for s in states]
+ good, revoked, unknown = (re.search(p, ocsp_output, flags=re.DOTALL) for p in patterns)
+
+ warning = good.group(1) if good else None
+
+ if (not "Response verify OK" in ocsp_errors) or (good and warning) or unknown:
+ logger.info("Revocation status for %s is unknown", cert_path)
+ logger.debug("Uncertain output:\n%s\nstderr:\n%s", ocsp_output, ocsp_errors)
+ return False
+ elif good and not warning:
+ return False
+ elif revoked:
+ warning = revoked.group(1)
+ if warning:
+ logger.info("OCSP revocation warning: %s", warning)
+ return True
+ else:
+ logger.warn("Unable to properly parse OCSP output: %s\nstderr:%s",
+ ocsp_output, ocsp_errors)
+ return False
+
diff --git a/certbot/plugins/common.py b/certbot/plugins/common.py
index 007105c7b..46d4c5740 100644
--- a/certbot/plugins/common.py
+++ b/certbot/plugins/common.py
@@ -127,17 +127,18 @@ class Addr(object):
return "%s:%s" % self.tup
return self.tup[0]
+ def normalized_tuple(self):
+ """Normalized representation of addr/port tuple
+ """
+ if self.ipv6:
+ return (self._normalize_ipv6(self.tup[0]), self.tup[1])
+ return self.tup
+
def __eq__(self, other):
if isinstance(other, self.__class__):
- if self.ipv6:
- # compare normalized to take different
- # styles of representation into account
- return (other.ipv6 and
- self._normalize_ipv6(self.tup[0]) ==
- self._normalize_ipv6(other.tup[0]) and
- self.tup[1] == other.tup[1])
- else:
- return self.tup == other.tup
+ # compare normalized to take different
+ # styles of representation into account
+ return self.normalized_tuple() == other.normalized_tuple()
return False
diff --git a/certbot/plugins/common_test.py b/certbot/plugins/common_test.py
index f3ea714c4..eee768e18 100644
--- a/certbot/plugins/common_test.py
+++ b/certbot/plugins/common_test.py
@@ -10,7 +10,7 @@ from acme import jose
from certbot import achallenges
from certbot.tests import acme_util
-from certbot.tests import test_util
+from certbot.tests import util as test_util
class NamespaceFunctionsTest(unittest.TestCase):
diff --git a/certbot/plugins/disco.py b/certbot/plugins/disco.py
index a6e8e7ed7..ba532eb1b 100644
--- a/certbot/plugins/disco.py
+++ b/certbot/plugins/disco.py
@@ -53,6 +53,14 @@ class PluginEntryPoint(object):
"""Description with name. Handy for UI."""
return "{0} ({1})".format(self.description, self.name)
+ @property
+ def long_description(self):
+ """Long description of the plugin."""
+ try:
+ return self.plugin_cls.long_description
+ except AttributeError:
+ return self.description
+
@property
def hidden(self):
"""Should this plugin be hidden from UI?"""
diff --git a/certbot/plugins/disco_test.py b/certbot/plugins/disco_test.py
index dadcde37d..7282c9ec8 100644
--- a/certbot/plugins/disco_test.py
+++ b/certbot/plugins/disco_test.py
@@ -63,6 +63,18 @@ class PluginEntryPointTest(unittest.TestCase):
self.assertEqual(
"Desc (sa)", self.plugin_ep.description_with_name)
+ def test_long_description(self):
+ self.plugin_ep.plugin_cls = mock.MagicMock(
+ long_description="Long desc")
+ self.assertEqual(
+ "Long desc", self.plugin_ep.long_description)
+
+ def test_long_description_nonexistent(self):
+ self.plugin_ep.plugin_cls = mock.MagicMock(
+ description="Long desc not found", spec=["description"])
+ self.assertEqual(
+ "Long desc not found", self.plugin_ep.long_description)
+
def test_ifaces(self):
self.assertTrue(self.plugin_ep.ifaces((interfaces.IAuthenticator,)))
self.assertFalse(self.plugin_ep.ifaces((interfaces.IInstaller,)))
diff --git a/certbot/plugins/manual.py b/certbot/plugins/manual.py
index 2ef49d7f4..1163e7e7e 100644
--- a/certbot/plugins/manual.py
+++ b/certbot/plugins/manual.py
@@ -1,57 +1,49 @@
-"""Manual plugin."""
+"""Manual authenticator plugin"""
import os
-import logging
-import pipes
-import shutil
-import signal
-import socket
-import subprocess
-import sys
-import tempfile
-import time
-import six
import zope.component
import zope.interface
from acme import challenges
-from acme import errors as acme_errors
-from certbot import errors
from certbot import interfaces
+from certbot import errors
+from certbot import hooks
from certbot.plugins import common
-logger = logging.getLogger(__name__)
-
-
@zope.interface.implementer(interfaces.IAuthenticator)
@zope.interface.provider(interfaces.IPluginFactory)
class Authenticator(common.Plugin):
- """Manual Authenticator.
+ """Manual authenticator
- This plugin requires user's manual intervention in setting up a HTTP
- server for solving http-01 challenges and thus does not need to be
- run as a privileged process. Alternatively shows instructions on how
- to use Python's built-in HTTP server.
-
- .. todo:: Support for `~.challenges.TLSSNI01`.
+ This plugin allows the user to perform the domain validation
+ challenge(s) themselves. This either be done manually by the user or
+ through shell scripts provided to Certbot.
"""
+
+ description = 'Manual configuration or run your own shell scripts'
hidden = True
-
- description = "Manually configure an HTTP server"
-
- MESSAGE_TEMPLATE = {
- "dns-01": """\
+ 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.')
+ _DNS_INSTRUCTIONS = """\
Please deploy a DNS TXT record under the name
{domain} with the following value:
{validation}
-Once this is deployed,
-""",
- "http-01": """\
+Once this is deployed,"""
+ _HTTP_INSTRUCTIONS = """\
Make sure your web server displays the following content at
{uri} before continuing:
@@ -60,203 +52,114 @@ Make sure your web server displays the following content at
If you don't have HTTP server configured, you can run the following
command on the target server (as root):
-{command}
-"""}
-
- # a disclaimer about your current IP being transmitted to Let's Encrypt's servers.
- IP_DISCLAIMER = """\
-NOTE: The IP of this machine will be publicly logged as having requested this certificate. \
-If you're running certbot in manual mode on a machine that is not your server, \
-please ensure you're okay with that.
-
-Are you OK with your IP being logged?
-"""
-
- # "cd /tmp/certbot" makes sure user doesn't serve /root,
- # separate "public_html" ensures that cert.pem/key.pem are not
- # served and makes it more obvious that Python command will serve
- # anything recursively under the cwd
-
- CMD_TEMPLATE = """\
-mkdir -p {root}/public_html/{achall.URI_ROOT_PATH}
-cd {root}/public_html
+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()" """
- """Command template."""
def __init__(self, *args, **kwargs):
super(Authenticator, self).__init__(*args, **kwargs)
- self._root = (tempfile.mkdtemp() if self.conf("test-mode")
- else "/tmp/certbot")
- self._httpd = None
+ self.env = dict()
@classmethod
def add_parser_arguments(cls, add):
- add("test-mode", action="store_true",
- help="Test mode. Executes the manual command in subprocess.")
- add("public-ip-logging-ok", action="store_true",
- help="Automatically allows public IP logging.")
+ add('auth-hook',
+ help='Path or command to execute for the authentication script')
+ add('cleanup-hook',
+ help='Path or command to execute for the cleanup script')
+ add('public-ip-logging-ok', action='store_true',
+ help='Automatically allows public IP logging (default: Ask)')
- def prepare(self): # pylint: disable=missing-docstring,no-self-use
- if self.config.noninteractive_mode and not self.conf("test-mode"):
- raise errors.PluginError("Running manual mode non-interactively is not supported")
+ def prepare(self): # pylint: disable=missing-docstring
+ if self.config.noninteractive_mode and not self.conf('auth-hook'):
+ raise errors.PluginError(
+ 'An authentication script must be provided with --{0} when '
+ 'using the manual plugin non-interactively.'.format(
+ self.option_name('auth-hook')))
+ self._validate_hooks()
+
+ def _validate_hooks(self):
+ if self.config.validate_hooks:
+ for name in ('auth-hook', 'cleanup-hook'):
+ hook = self.conf(name)
+ if hook is not None:
+ hook_prefix = self.option_name(name)[:-len('-hook')]
+ hooks.validate_hook(hook, hook_prefix)
def more_info(self): # pylint: disable=missing-docstring,no-self-use
- return ("This plugin requires user's manual intervention in setting "
- "up challenges to prove control of a domain and does not need "
- "to be run as a privileged process. When solving "
- "http-01 challenges, the user is responsible for setting up "
- "an HTTP server. Alternatively, instructions are shown on how "
- "to use Python's built-in HTTP server. The user is "
- "responsible for configuration of a domain's DNS when solving "
- "dns-01 challenges. The type of challenges used can be "
- "controlled through the --preferred-challenges flag.")
+ return (
+ 'This plugin allows the user to customize setup for domain '
+ 'validation challenges either through shell scripts provided by '
+ 'the user or by performing the setup manually.')
def get_chall_pref(self, domain):
# pylint: disable=missing-docstring,no-self-use,unused-argument
return [challenges.HTTP01, challenges.DNS01]
- def perform(self, achalls):
- # pylint: disable=missing-docstring
- self._get_ip_logging_permission()
- mapping = {"http-01": self._perform_http01_challenge,
- "dns-01": self._perform_dns01_challenge}
+ 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:
+ perform_achall = self._perform_achall_manually
+
responses = []
- # TODO: group achalls by the same socket.gethostbyname(_ex)
- # and prompt only once per server (one "echo -n" per domain)
for achall in achalls:
- responses.append(mapping[achall.typ](achall))
+ perform_achall(achall)
+ responses.append(achall.response(achall.account_key))
return responses
- @classmethod
- def _test_mode_busy_wait(cls, port):
- while True:
- time.sleep(1)
- sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- try:
- sock.connect(("localhost", port))
- except socket.error: # pragma: no cover
- pass
+ def _verify_ip_logging_ok(self):
+ if not self.conf('public-ip-logging-ok'):
+ cli_flag = '--{0}'.format(self.option_name('public-ip-logging-ok'))
+ msg = ('NOTE: The IP of this machine will be publicly logged as '
+ "having requested this certificate. If you're running "
+ 'certbot in manual mode on a machine that is not your '
+ "server, please ensure you're okay with that.\n\n"
+ 'Are you OK with your IP being logged?')
+ display = zope.component.getUtility(interfaces.IDisplay)
+ if display.yesno(msg, cli_flag=cli_flag, force_interactive=True):
+ setattr(self.config, self.dest('public-ip-logging-ok'), True)
else:
- break
- finally:
- sock.close()
+ raise errors.PluginError('Must agree to IP logging to proceed')
- def cleanup(self, achalls):
- # pylint: disable=missing-docstring
- for achall in achalls:
- if isinstance(achall.chall, challenges.HTTP01):
- self._cleanup_http01_challenge(achall)
-
- def _perform_http01_challenge(self, achall):
- # same path for each challenge response would be easier for
- # users, but will not work if multiple domains point at the
- # same server: default command doesn't support virtual hosts
- response, validation = achall.response_and_validation()
-
- port = (response.port if self.config.http01_port is None
- else int(self.config.http01_port))
- command = self.CMD_TEMPLATE.format(
- root=self._root, achall=achall, response=response,
- # TODO(kuba): pipes still necessary?
- validation=pipes.quote(validation),
- encoded_token=achall.chall.encode("token"),
- port=port)
- if self.conf("test-mode"):
- logger.debug("Test mode. Executing the manual command: %s", command)
- # sh shipped with OS X does't support echo -n, but supports printf
- try:
- self._httpd = subprocess.Popen(
- command,
- # don't care about setting stdout and stderr,
- # we're in test mode anyway
- shell=True,
- executable=None,
- # "preexec_fn" is UNIX specific, but so is "command"
- preexec_fn=os.setsid)
- except OSError as error: # ValueError should not happen!
- logger.debug(
- "Couldn't execute manual command: %s", error, exc_info=True)
- return False
- logger.debug("Manual command running as PID %s.", self._httpd.pid)
- # give it some time to bootstrap, before we try to verify
- # (cert generation in case of simpleHttpS might take time)
- self._test_mode_busy_wait(port)
-
- if self._httpd.poll() is not None:
- raise errors.Error("Couldn't execute manual command")
+ def _perform_achall_with_script(self, achall):
+ env = dict(CERTBOT_DOMAIN=achall.domain,
+ CERTBOT_VALIDATION=achall.validation(achall.account_key))
+ if isinstance(achall.chall, challenges.HTTP01):
+ env['CERTBOT_TOKEN'] = achall.chall.encode('token')
else:
- self._notify_and_wait(
- self._get_message(achall).format(
- validation=validation,
- response=response,
- uri=achall.chall.uri(achall.domain),
- command=command))
+ os.environ.pop('CERTBOT_TOKEN', None)
+ os.environ.update(env)
+ _, out = hooks.execute(self.conf('auth-hook'))
+ env['CERTBOT_AUTH_OUTPUT'] = out.strip()
+ self.env[achall.domain] = env
- if not response.simple_verify(
- achall.chall, achall.domain,
- achall.account_key.public_key(), self.config.http01_port):
- logger.warning("Self-verify of challenge failed.")
-
- return response
-
- def _perform_dns01_challenge(self, achall):
- response, validation = achall.response_and_validation()
- if not self.conf("test-mode"):
- self._notify_and_wait(
- self._get_message(achall).format(
- validation=validation,
- domain=achall.validation_domain_name(achall.domain),
- response=response))
-
- try:
- verification_status = response.simple_verify(
- achall.chall, achall.domain,
- achall.account_key.public_key())
- except acme_errors.DependencyError:
- logger.warning("Self verification requires optional "
- "dependency `dnspython` to be installed.")
+ def _perform_achall_manually(self, achall):
+ validation = achall.validation(achall.account_key)
+ if isinstance(achall.chall, challenges.HTTP01):
+ msg = self._HTTP_INSTRUCTIONS.format(
+ achall=achall, encoded_token=achall.chall.encode('token'),
+ port=self.config.http01_port,
+ uri=achall.chall.uri(achall.domain), validation=validation)
else:
- if not verification_status:
- logger.warning("Self-verify of challenge failed.")
+ assert isinstance(achall.chall, challenges.DNS01)
+ msg = self._DNS_INSTRUCTIONS.format(
+ domain=achall.validation_domain_name(achall.domain),
+ validation=validation)
+ display = zope.component.getUtility(interfaces.IDisplay)
+ display.notification(msg, wrap=False, force_interactive=True)
- return response
-
- def _cleanup_http01_challenge(self, achall):
- # pylint: disable=missing-docstring,unused-argument
- if self.conf("test-mode"):
- assert self._httpd is not None, (
- "cleanup() must be called after perform()")
- if self._httpd.poll() is None:
- logger.debug("Terminating manual command process")
- os.killpg(self._httpd.pid, signal.SIGTERM)
- else:
- logger.debug("Manual command process already terminated "
- "with %s code", self._httpd.returncode)
- shutil.rmtree(self._root)
-
- def _notify_and_wait(self, message):
- # pylint: disable=no-self-use
- # TODO: IDisplay wraps messages, breaking the command
- #answer = zope.component.getUtility(interfaces.IDisplay).notification(
- # message=message, height=25, pause=True)
- sys.stdout.write(message)
- six.moves.input("Press ENTER to continue")
-
- def _get_ip_logging_permission(self):
- # pylint: disable=missing-docstring
- if not (self.conf("test-mode") or self.conf("public-ip-logging-ok")):
- if not zope.component.getUtility(interfaces.IDisplay).yesno(
- self.IP_DISCLAIMER, "Yes", "No",
- cli_flag="--manual-public-ip-logging-ok"):
- raise errors.PluginError("Must agree to IP logging to proceed")
- else:
- self.config.namespace.manual_public_ip_logging_ok = True
-
- def _get_message(self, achall):
- # pylint: disable=missing-docstring,no-self-use,unused-argument
- return self.MESSAGE_TEMPLATE.get(achall.chall.typ, "")
+ def cleanup(self, achalls): # pylint: disable=missing-docstring
+ if self.conf('cleanup-hook'):
+ for achall in achalls:
+ env = self.env.pop(achall.domain)
+ if 'CERTBOT_TOKEN' not in env:
+ os.environ.pop('CERTBOT_TOKEN', None)
+ os.environ.update(env)
+ hooks.execute(self.conf('cleanup-hook'))
diff --git a/certbot/plugins/manual_test.py b/certbot/plugins/manual_test.py
index 25107e4b4..bd6816f02 100644
--- a/certbot/plugins/manual_test.py
+++ b/certbot/plugins/manual_test.py
@@ -1,136 +1,114 @@
-"""Tests for certbot.plugins.manual."""
-import signal
+"""Tests for certbot.plugins.manual"""
+import os
import unittest
+import six
import mock
from acme import challenges
-from acme import errors as acme_errors
-from acme import jose
-from certbot import achallenges
from certbot import errors
from certbot.tests import acme_util
-from certbot.tests import test_util
-
-
-KEY = jose.JWKRSA.load(test_util.load_vector("rsa512_key.pem"))
+from certbot.tests import util as test_util
class AuthenticatorTest(unittest.TestCase):
"""Tests for certbot.plugins.manual.Authenticator."""
def setUp(self):
- from certbot.plugins.manual import Authenticator
+ self.http_achall = acme_util.HTTP01_A
+ self.dns_achall = acme_util.DNS01_A
+ self.achalls = [self.http_achall, self.dns_achall]
self.config = mock.MagicMock(
- http01_port=8080, manual_test_mode=False,
- manual_public_ip_logging_ok=False, noninteractive_mode=True)
- self.auth = Authenticator(config=self.config, name="manual")
+ http01_port=0, manual_auth_hook=None, manual_cleanup_hook=None,
+ manual_public_ip_logging_ok=False, noninteractive_mode=False,
+ validate_hooks=False)
- self.http01 = achallenges.KeyAuthorizationAnnotatedChallenge(
- challb=acme_util.HTTP01_P, domain="foo.com", account_key=KEY)
- self.dns01 = achallenges.KeyAuthorizationAnnotatedChallenge(
- challb=acme_util.DNS01_P, domain="foo.com", account_key=KEY)
+ from certbot.plugins.manual import Authenticator
+ self.auth = Authenticator(self.config, name='manual')
- self.achalls = [self.http01, self.dns01]
-
- config_test_mode = mock.MagicMock(
- http01_port=8080, manual_test_mode=True, noninteractive_mode=True)
- self.auth_test_mode = Authenticator(
- config=config_test_mode, name="manual")
-
- def test_prepare(self):
+ def test_prepare_no_hook_noninteractive(self):
+ self.config.noninteractive_mode = True
self.assertRaises(errors.PluginError, self.auth.prepare)
- self.auth_test_mode.prepare() # error not raised
+
+ def test_prepare_bad_hook(self):
+ self.config.manual_auth_hook = os.path.abspath(os.sep) # is / on UNIX
+ self.config.validate_hooks = True
+ self.assertRaises(errors.HookCommandNotFound, self.auth.prepare)
def test_more_info(self):
- self.assertTrue(isinstance(self.auth.more_info(), str))
+ self.assertTrue(isinstance(self.auth.more_info(), six.string_types))
def test_get_chall_pref(self):
- self.assertTrue(all(issubclass(pref, challenges.Challenge)
- for pref in self.auth.get_chall_pref("foo.com")))
+ self.assertEqual(self.auth.get_chall_pref('example.org'),
+ [challenges.HTTP01, challenges.DNS01])
- @mock.patch("certbot.plugins.manual.zope.component.getUtility")
- def test_perform_empty(self, mock_interaction):
- mock_interaction().yesno.return_value = True
- self.assertEqual([], self.auth.perform([]))
+ @test_util.patch_get_utility()
+ def test_ip_logging_not_ok(self, mock_get_utility):
+ mock_get_utility().yesno.return_value = False
+ self.assertRaises(errors.PluginError, self.auth.perform, [])
- @mock.patch("certbot.plugins.manual.zope.component.getUtility")
- @mock.patch("certbot.plugins.manual.sys.stdout")
- @mock.patch("acme.challenges.HTTP01Response.simple_verify")
- @mock.patch("six.moves.input")
- def test_perform(self, mock_raw_input, mock_verify, mock_stdout, mock_interaction):
- mock_verify.return_value = True
- mock_interaction().yesno.return_value = True
+ @test_util.patch_get_utility()
+ def test_ip_logging_ok(self, mock_get_utility):
+ mock_get_utility().yesno.return_value = True
+ self.auth.perform([])
+ self.assertTrue(self.config.manual_public_ip_logging_ok)
- resp_http = self.http01.response(KEY)
- resp_dns = self.dns01.response(KEY)
+ 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(
+ self.dns_achall.domain, 'notoken',
+ self.dns_achall.validation(self.dns_achall.account_key))
+ http_expected = '{0}\n{1}\n{2}'.format(
+ self.http_achall.domain, self.http_achall.chall.encode('token'),
+ self.http_achall.validation(self.http_achall.account_key))
- self.assertEqual([resp_http, resp_dns], self.auth.perform(self.achalls))
- self.assertEqual(2, mock_raw_input.call_count)
- mock_verify.assert_called_with(
- self.http01.challb.chall, "foo.com", KEY.public_key(), 8080)
+ self.assertEqual(
+ self.auth.perform(self.achalls),
+ [achall.response(achall.account_key) for achall in self.achalls])
+ self.assertEqual(
+ self.auth.env[self.dns_achall.domain]['CERTBOT_AUTH_OUTPUT'],
+ dns_expected)
+ self.assertEqual(
+ self.auth.env[self.http_achall.domain]['CERTBOT_AUTH_OUTPUT'],
+ http_expected)
- message = mock_stdout.write.mock_calls[0][1][0]
- self.assertTrue(self.http01.chall.encode("token") in message)
+ @test_util.patch_get_utility()
+ def test_manual_perform(self, mock_get_utility):
+ self.config.manual_public_ip_logging_ok = True
+ self.assertEqual(
+ self.auth.perform(self.achalls),
+ [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])
+ self.assertFalse(kwargs['wrap'])
- mock_verify.return_value = False
- with mock.patch("certbot.plugins.manual.logger") as mock_logger:
- self.auth.perform(self.achalls)
- self.assertEqual(2, mock_logger.warning.call_count)
+ def test_cleanup(self):
+ self.config.manual_public_ip_logging_ok = True
+ self.config.manual_auth_hook = 'echo foo;'
+ self.config.manual_cleanup_hook = '# cleanup'
+ self.auth.perform(self.achalls)
- @mock.patch("certbot.plugins.manual.zope.component.getUtility")
- @mock.patch("acme.challenges.DNS01Response.simple_verify")
- @mock.patch("six.moves.input")
- def test_perform_missing_dependency(self, mock_raw_input, mock_verify, mock_interaction):
- mock_interaction().yesno.return_value = True
- mock_verify.side_effect = acme_errors.DependencyError()
+ for achall in self.achalls:
+ 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))
- with mock.patch("certbot.plugins.manual.logger") as mock_logger:
- self.auth.perform([self.dns01])
- self.assertEqual(1, mock_logger.warning.call_count)
-
- mock_raw_input.assert_called_once_with("Press ENTER to continue")
-
- @mock.patch("certbot.plugins.manual.zope.component.getUtility")
- @mock.patch("certbot.plugins.manual.Authenticator._notify_and_wait")
- def test_disagree_with_ip_logging(self, mock_notify, mock_interaction):
- mock_interaction().yesno.return_value = False
- mock_notify.side_effect = errors.Error("Exception not raised, \
- continued execution even after disagreeing with IP logging")
-
- self.assertRaises(errors.PluginError, self.auth.perform, self.achalls)
-
- @mock.patch("certbot.plugins.manual.subprocess.Popen", autospec=True)
- def test_perform_test_command_oserror(self, mock_popen):
- mock_popen.side_effect = OSError
- self.assertEqual([False], self.auth_test_mode.perform([self.http01]))
-
- @mock.patch("certbot.plugins.manual.socket.socket")
- @mock.patch("certbot.plugins.manual.time.sleep", autospec=True)
- @mock.patch("certbot.plugins.manual.subprocess.Popen", autospec=True)
- def test_perform_test_command_run_failure(
- self, mock_popen, unused_mock_sleep, unused_mock_socket):
- mock_popen.poll.return_value = 10
- mock_popen.return_value.pid = 1234
- self.assertRaises(
- errors.Error, self.auth_test_mode.perform, self.achalls)
-
- def test_cleanup_test_mode_already_terminated(self):
- # pylint: disable=protected-access
- self.auth_test_mode._httpd = httpd = mock.Mock()
- httpd.poll.return_value = 0
- self.auth_test_mode.cleanup(self.achalls)
-
- @mock.patch("certbot.plugins.manual.os.killpg", autospec=True)
- def test_cleanup_test_mode_kills_still_running(self, mock_killpg):
- # pylint: disable=protected-access
- self.auth_test_mode._httpd = httpd = mock.Mock(pid=1234)
- httpd.poll.return_value = None
- self.auth_test_mode.cleanup(self.achalls)
- mock_killpg.assert_called_once_with(1234, signal.SIGTERM)
+ 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 __name__ == "__main__":
+if __name__ == '__main__':
unittest.main() # pragma: no cover
diff --git a/certbot/plugins/null.py b/certbot/plugins/null.py
index 995b96274..87c0737a5 100644
--- a/certbot/plugins/null.py
+++ b/certbot/plugins/null.py
@@ -40,9 +40,6 @@ class Installer(common.Plugin):
def supported_enhancements(self):
return []
- def get_all_certs_keys(self):
- return []
-
def save(self, title=None, temporary=False):
pass # pragma: no cover
diff --git a/certbot/plugins/null_test.py b/certbot/plugins/null_test.py
index 305954a2f..0d04a2bc5 100644
--- a/certbot/plugins/null_test.py
+++ b/certbot/plugins/null_test.py
@@ -15,7 +15,6 @@ class InstallerTest(unittest.TestCase):
self.assertTrue(isinstance(self.installer.more_info(), str))
self.assertEqual([], self.installer.get_all_names())
self.assertEqual([], self.installer.supported_enhancements())
- self.assertEqual([], self.installer.get_all_certs_keys())
if __name__ == "__main__":
diff --git a/certbot/plugins/selection.py b/certbot/plugins/selection.py
index 3fbc510ba..81387c435 100644
--- a/certbot/plugins/selection.py
+++ b/certbot/plugins/selection.py
@@ -111,7 +111,8 @@ def choose_plugin(prepared, question):
while True:
disp = z_util(interfaces.IDisplay)
- code, index = disp.menu(question, opts, help_label="More Info")
+ code, index = disp.menu(
+ question, opts, help_label="More Info", force_interactive=True)
if code == display_util.OK:
plugin_ep = prepared[index]
@@ -119,8 +120,7 @@ def choose_plugin(prepared, question):
z_util(interfaces.IDisplay).notification(
"The selected plugin encountered an error while parsing "
"your server configuration and cannot be used. The error "
- "was:\n\n{0}".format(plugin_ep.prepare()),
- height=display_util.HEIGHT, pause=False)
+ "was:\n\n{0}".format(plugin_ep.prepare()), pause=False)
else:
return plugin_ep
elif code == display_util.HELP:
@@ -128,8 +128,8 @@ def choose_plugin(prepared, question):
msg = "Reported Error: %s" % prepared[index].prepare()
else:
msg = prepared[index].init().more_info()
- z_util(interfaces.IDisplay).notification(
- msg, height=display_util.HEIGHT)
+ z_util(interfaces.IDisplay).notification(msg,
+ force_interactive=True)
else:
return None
diff --git a/certbot/plugins/selection_test.py b/certbot/plugins/selection_test.py
index 001ca5cff..c0494e565 100644
--- a/certbot/plugins/selection_test.py
+++ b/certbot/plugins/selection_test.py
@@ -110,7 +110,8 @@ class ChoosePluginTest(unittest.TestCase):
"""Tests for certbot.plugins.selection.choose_plugin."""
def setUp(self):
- zope.component.provideUtility(display_util.FileDisplay(sys.stdout))
+ zope.component.provideUtility(display_util.FileDisplay(sys.stdout,
+ False))
self.mock_apache = mock.Mock(
description_with_name="a", misconfigured=True)
self.mock_stand = mock.Mock(
diff --git a/certbot/plugins/standalone.py b/certbot/plugins/standalone.py
index e8c11a416..4fc52479f 100644
--- a/certbot/plugins/standalone.py
+++ b/certbot/plugins/standalone.py
@@ -243,13 +243,13 @@ class Authenticator(common.Plugin):
"Could not bind TCP port {0} because you don't have "
"the appropriate permissions (for example, you "
"aren't running this program as "
- "root).".format(error.port))
+ "root).".format(error.port), force_interactive=True)
elif error.socket_error.errno == socket.errno.EADDRINUSE:
display.notification(
"Could not bind TCP port {0} because it is already in "
"use by another process on this system (such as a web "
"server). Please stop the program in question and then "
- "try again.".format(error.port))
+ "try again.".format(error.port), force_interactive=True)
else:
raise # XXX: How to handle unknown errors in binding?
diff --git a/certbot/plugins/standalone_test.py b/certbot/plugins/standalone_test.py
index cb82ae7d8..08e59c929 100644
--- a/certbot/plugins/standalone_test.py
+++ b/certbot/plugins/standalone_test.py
@@ -15,7 +15,7 @@ from certbot import errors
from certbot import interfaces
from certbot.tests import acme_util
-from certbot.tests import test_util
+from certbot.tests import util as test_util
class ServerManagerTest(unittest.TestCase):
@@ -169,7 +169,7 @@ class AuthenticatorTest(unittest.TestCase):
mock_util.already_listening.assert_called_once_with(port, False)
mock_util.already_listening.reset_mock()
- @mock.patch("certbot.plugins.standalone.zope.component.getUtility")
+ @test_util.patch_get_utility()
def test_perform(self, unused_mock_get_utility):
achalls = self._get_achalls()
@@ -177,7 +177,7 @@ class AuthenticatorTest(unittest.TestCase):
self.assertEqual(mock.sentinel.responses, self.auth.perform(achalls))
self.auth.perform2.assert_called_once_with(achalls)
- @mock.patch("certbot.plugins.standalone.zope.component.getUtility")
+ @test_util.patch_get_utility()
def _test_perform_bind_errors(self, errno, achalls, mock_get_utility):
port = get_open_port()
def _perform2(unused_achalls):
diff --git a/certbot/plugins/util.py b/certbot/plugins/util.py
index 786f6ca92..20b0fdce7 100644
--- a/certbot/plugins/util.py
+++ b/certbot/plugins/util.py
@@ -30,12 +30,12 @@ RENEWER_EXTRA_MSG = (
" needing to stop and start your webserver.")
-def path_surgery(restart_cmd):
- """Attempt to perform PATH surgery to find restart_cmd
+def path_surgery(cmd):
+ """Attempt to perform PATH surgery to find cmd
Mitigates https://github.com/certbot/certbot/issues/1833
- :param str restart_cmd: the command that is being searched for in the PATH
+ :param str cmd: the command that is being searched for in the PATH
:returns: True if the operation succeeded, False otherwise
"""
@@ -49,14 +49,14 @@ def path_surgery(restart_cmd):
if any(added):
logger.debug("Can't find %s, attempting PATH mitigation by adding %s",
- restart_cmd, os.pathsep.join(added))
+ cmd, os.pathsep.join(added))
os.environ["PATH"] = path
- if util.exe_exists(restart_cmd):
+ if util.exe_exists(cmd):
return True
else:
expanded = " expanded" if any(added) else ""
- logger.warning("Failed to find %s in%s PATH: %s", restart_cmd,
+ logger.warning("Failed to find %s in%s PATH: %s", cmd,
expanded, path)
return False
@@ -103,7 +103,7 @@ def already_listening_socket(port, renewer=False):
"Port {0} is already in use by another process. This will "
"prevent us from binding to that port. Please stop the "
"process that is populating the port in question and try "
- "again. {1}".format(port, extra), height=13)
+ "again. {1}".format(port, extra), force_interactive=True)
return True
finally:
testsocket.close()
@@ -152,7 +152,7 @@ def already_listening_psutil(port, renewer=False):
"on TCP port {2}. This will prevent us from binding to "
"that port. Please stop the {0} program temporarily "
"and then try again.{3}".format(name, pid, port, extra),
- height=13)
+ force_interactive=True)
return True
except (psutil.NoSuchProcess, psutil.AccessDenied):
# Perhaps the result of a race where the process could have
diff --git a/certbot/plugins/util_test.py b/certbot/plugins/util_test.py
index f8ffede86..b5d188835 100644
--- a/certbot/plugins/util_test.py
+++ b/certbot/plugins/util_test.py
@@ -6,7 +6,7 @@ import unittest
import mock
from certbot.plugins.util import PSUTIL_REQUIREMENT
-from certbot.tests import test_util
+from certbot.tests import util as test_util
class PathSurgeryTest(unittest.TestCase):
@@ -51,7 +51,7 @@ class AlreadyListeningTestNoPsutil(AlreadyListeningTest):
return super(
AlreadyListeningTestNoPsutil, cls)._call(*args, **kwargs)
- @mock.patch("certbot.plugins.util.zope.component.getUtility")
+ @test_util.patch_get_utility()
def test_ports_available(self, mock_getutil):
# Ensure we don't get error
with mock.patch("socket.socket.bind"):
@@ -59,7 +59,7 @@ class AlreadyListeningTestNoPsutil(AlreadyListeningTest):
self.assertFalse(self._call(80, True))
self.assertEqual(mock_getutil.call_count, 0)
- @mock.patch("certbot.plugins.util.zope.component.getUtility")
+ @test_util.patch_get_utility()
def test_ports_blocked(self, mock_getutil):
with mock.patch("certbot.plugins.util.socket.socket.bind") as mock_bind:
mock_bind.side_effect = socket.error
@@ -77,7 +77,7 @@ class AlreadyListeningTestPsutil(AlreadyListeningTest):
"""Tests for certbot.plugins.already_listening."""
@mock.patch("certbot.plugins.util.psutil.net_connections")
@mock.patch("certbot.plugins.util.psutil.Process")
- @mock.patch("certbot.plugins.util.zope.component.getUtility")
+ @test_util.patch_get_utility()
def test_race_condition(self, mock_get_utility, mock_process, mock_net):
# This tests a race condition, or permission problem, or OS
# incompatibility in which, for some reason, no process name can be
@@ -103,7 +103,7 @@ class AlreadyListeningTestPsutil(AlreadyListeningTest):
@mock.patch("certbot.plugins.util.psutil.net_connections")
@mock.patch("certbot.plugins.util.psutil.Process")
- @mock.patch("certbot.plugins.util.zope.component.getUtility")
+ @test_util.patch_get_utility()
def test_not_listening(self, mock_get_utility, mock_process, mock_net):
from psutil._common import sconn
conns = [
@@ -121,7 +121,7 @@ class AlreadyListeningTestPsutil(AlreadyListeningTest):
@mock.patch("certbot.plugins.util.psutil.net_connections")
@mock.patch("certbot.plugins.util.psutil.Process")
- @mock.patch("certbot.plugins.util.zope.component.getUtility")
+ @test_util.patch_get_utility()
def test_listening_ipv4(self, mock_get_utility, mock_process, mock_net):
from psutil._common import sconn
conns = [
@@ -142,7 +142,7 @@ class AlreadyListeningTestPsutil(AlreadyListeningTest):
@mock.patch("certbot.plugins.util.psutil.net_connections")
@mock.patch("certbot.plugins.util.psutil.Process")
- @mock.patch("certbot.plugins.util.zope.component.getUtility")
+ @test_util.patch_get_utility()
def test_listening_ipv6(self, mock_get_utility, mock_process, mock_net):
from psutil._common import sconn
conns = [
diff --git a/certbot/plugins/webroot.py b/certbot/plugins/webroot.py
index 1cd1d879a..b3ec4a692 100644
--- a/certbot/plugins/webroot.py
+++ b/certbot/plugins/webroot.py
@@ -45,7 +45,7 @@ to serve all files under specified web root ({0})."""
"times to handle different domains; each domain will have "
"the webroot path that preceded it. For instance: `-w "
"/var/www/example -d example.com -d www.example.com -w "
- "/var/www/thing -d thing.net -d m.thing.net`")
+ "/var/www/thing -d thing.net -d m.thing.net` (default: Ask)")
add("map", default={}, action=_WebrootMapAction,
help="JSON dictionary mapping domains to webroot paths; this "
"implies -d for each entry. You may need to escape this from "
@@ -110,12 +110,13 @@ to serve all files under specified web root ({0})."""
def _prompt_with_webroot_list(self, domain, known_webroots):
display = zope.component.getUtility(interfaces.IDisplay)
+ path_flag = "--" + self.option_name("path")
while True:
code, index = display.menu(
"Select the webroot for {0}:".format(domain),
["Enter a new webroot"] + known_webroots,
- help_label="Help", cli_flag="--" + self.option_name("path"))
+ help_label="Help", cli_flag=path_flag, force_interactive=True)
if code == display_util.CANCEL:
raise errors.PluginError(
"Every requested domain must have a "
@@ -129,7 +130,8 @@ to serve all files under specified web root ({0})."""
"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.")
+ "to pass domain validation challenges.",
+ force_interactive=True)
else: # code == display_util.OK
return None if index == 0 else known_webroots[index - 1]
@@ -138,11 +140,11 @@ to serve all files under specified web root ({0})."""
while True:
code, webroot = display.directory_select(
- "Input the webroot for {0}:".format(domain))
+ "Input the webroot for {0}:".format(domain),
+ force_interactive=True)
if code == display_util.HELP:
- # Help can currently only be selected
- # when using the ncurses interface
- display.notification(display_util.DSELECT_HELP)
+ # Displaying help is not currently implemented
+ return None
elif code == display_util.CANCEL:
return None
else: # code == display_util.OK
diff --git a/certbot/plugins/webroot_test.py b/certbot/plugins/webroot_test.py
index 5d784a75c..3e9a68b84 100644
--- a/certbot/plugins/webroot_test.py
+++ b/certbot/plugins/webroot_test.py
@@ -21,7 +21,7 @@ from certbot import errors
from certbot.display import util as display_util
from certbot.tests import acme_util
-from certbot.tests import test_util
+from certbot.tests import util as test_util
KEY = jose.JWKRSA.load(test_util.load_vector("rsa512_key.pem"))
@@ -61,7 +61,7 @@ class AuthenticatorTest(unittest.TestCase):
def test_prepare(self):
self.auth.prepare() # shouldn't raise any exceptions
- @mock.patch("certbot.plugins.webroot.zope.component.getUtility")
+ @test_util.patch_get_utility()
def test_webroot_from_list(self, mock_get_utility):
self.config.webroot_path = []
self.config.webroot_map = {"otherthing.com": self.path}
@@ -78,7 +78,7 @@ class AuthenticatorTest(unittest.TestCase):
self.assertEqual(self.config.webroot_map[self.achall.domain],
self.path)
- @mock.patch("certbot.plugins.webroot.zope.component.getUtility")
+ @test_util.patch_get_utility()
def test_webroot_from_list_help_and_cancel(self, mock_get_utility):
self.config.webroot_path = []
self.config.webroot_map = {"otherthing.com": self.path}
@@ -95,7 +95,7 @@ class AuthenticatorTest(unittest.TestCase):
webroot in call[0][1]
for webroot in six.itervalues(self.config.webroot_map)))
- @mock.patch("certbot.plugins.webroot.zope.component.getUtility")
+ @test_util.patch_get_utility()
def test_new_webroot(self, mock_get_utility):
self.config.webroot_path = []
self.config.webroot_map = {}
@@ -111,8 +111,7 @@ class AuthenticatorTest(unittest.TestCase):
self.assertTrue(mock_display.notification.called)
for call in mock_display.notification.call_args_list:
- self.assertTrue(imaginary_dir in call[0][0] or
- display_util.DSELECT_HELP == call[0][0])
+ self.assertTrue(imaginary_dir in call[0][0])
self.assertTrue(mock_display.directory_select.called)
for call in mock_display.directory_select.call_args_list:
diff --git a/certbot/renewal.py b/certbot/renewal.py
index 5e57c3e20..d65cd4904 100644
--- a/certbot/renewal.py
+++ b/certbot/renewal.py
@@ -1,7 +1,7 @@
"""Functionality for autorenewal and associated juggling of configurations"""
from __future__ import print_function
import copy
-import glob
+import itertools
import logging
import os
import traceback
@@ -11,9 +11,7 @@ import zope.component
import OpenSSL
-from certbot import configuration
from certbot import cli
-from certbot import constants
from certbot import crypto_util
from certbot import errors
@@ -31,20 +29,20 @@ logger = logging.getLogger(__name__)
# the renewal configuration process loses this information.
STR_CONFIG_ITEMS = ["config_dir", "logs_dir", "work_dir", "user_agent",
"server", "account", "authenticator", "installer",
- "standalone_supported_challenges"]
+ "standalone_supported_challenges", "renew_hook",
+ "pre_hook", "post_hook"]
INT_CONFIG_ITEMS = ["rsa_key_size", "tls_sni_01_port", "http01_port"]
+BOOL_CONFIG_ITEMS = ["must_staple", "allow_subset_of_names"]
-
-def renewal_conf_files(config):
- """Return /path/to/*.conf in the renewal conf directory"""
- return glob.glob(os.path.join(config.renewal_configs_dir, "*.conf"))
+CONFIG_ITEMS = set(itertools.chain(
+ BOOL_CONFIG_ITEMS, INT_CONFIG_ITEMS, STR_CONFIG_ITEMS))
def _reconstitute(config, full_path):
"""Try to instantiate a RenewableCert, updating config with relevant items.
This is specifically for use in renewal and enforces several checks
- and policies to ensure that we can try to proceed with the renwal
+ and policies to ensure that we can try to proceed with the renewal
request. The config argument is modified by including relevant options
read from the renewal configuration file.
@@ -58,8 +56,7 @@ def _reconstitute(config, full_path):
"""
try:
- renewal_candidate = storage.RenewableCert(
- full_path, configuration.RenewerConfiguration(config))
+ renewal_candidate = storage.RenewableCert(full_path, config)
except (errors.CertStorageError, IOError) as exc:
logger.warning(exc)
logger.warning("Renewal configuration file %s is broken. Skipping.", full_path)
@@ -77,7 +74,7 @@ def _reconstitute(config, full_path):
# Now restore specific values along with their data types, if
# those elements are present.
try:
- _restore_required_config_elements(config, renewalparams)
+ restore_required_config_elements(config, renewalparams)
_restore_plugin_configs(config, renewalparams)
except (ValueError, errors.Error) as error:
logger.warning(
@@ -157,7 +154,7 @@ def _restore_plugin_configs(config, renewalparams):
setattr(config.namespace, config_item, cast(config_value))
-def _restore_required_config_elements(config, renewalparams):
+def restore_required_config_elements(config, renewalparams):
"""Sets non-plugin specific values in config from renewalparams
:param configuration.NamespaceConfig config: configuration for the
@@ -166,30 +163,69 @@ def _restore_required_config_elements(config, renewalparams):
configuration file that defines this lineage
"""
- # string-valued items to add if they're present
- for config_item in STR_CONFIG_ITEMS:
- if config_item in renewalparams and not cli.set_by_cli(config_item):
- value = renewalparams[config_item]
- # Unfortunately, we've lost type information from ConfigObj,
- # so we don't know if the original was NoneType or str!
- if value == "None":
- value = None
- setattr(config.namespace, config_item, value)
- # int-valued items to add if they're present
- for config_item in INT_CONFIG_ITEMS:
- if config_item in renewalparams and not cli.set_by_cli(config_item):
- config_value = renewalparams[config_item]
- # the default value for http01_port was None during private beta
- if config_item == "http01_port" and config_value == "None":
- logger.info("updating legacy http01_port value")
- int_value = cli.flag_default("http01_port")
- else:
- try:
- int_value = int(config_value)
- except ValueError:
- raise errors.Error(
- "Expected a numeric value for {0}".format(config_item))
- setattr(config.namespace, config_item, int_value)
+
+ required_items = itertools.chain(
+ six.moves.zip(BOOL_CONFIG_ITEMS, itertools.repeat(_restore_bool)),
+ six.moves.zip(INT_CONFIG_ITEMS, itertools.repeat(_restore_int)),
+ six.moves.zip(STR_CONFIG_ITEMS, itertools.repeat(_restore_str)))
+ for item_name, restore_func in required_items:
+ if item_name in renewalparams and not cli.set_by_cli(item_name):
+ value = restore_func(item_name, renewalparams[item_name])
+ setattr(config.namespace, item_name, value)
+
+
+def _restore_bool(name, value):
+ """Restores an boolean key-value pair from a renewal config file.
+
+ :param str name: option name
+ :param str value: option value
+
+ :returns: converted option value to be stored in the runtime config
+ :rtype: bool
+
+ :raises errors.Error: if value can't be converted to an bool
+
+ """
+ lowercase_value = value.lower()
+ if lowercase_value not in ("true", "false"):
+ raise errors.Error(
+ "Expected True or False for {0} but found {1}".format(name, value))
+ return lowercase_value == "true"
+
+
+def _restore_int(name, value):
+ """Restores an integer key-value pair from a renewal config file.
+
+ :param str name: option name
+ :param str value: option value
+
+ :returns: converted option value to be stored in the runtime config
+ :rtype: int
+
+ :raises errors.Error: if value can't be converted to an int
+
+ """
+ if name == "http01_port" and value == "None":
+ logger.info("updating legacy http01_port value")
+ return cli.flag_default("http01_port")
+
+ try:
+ return int(value)
+ except ValueError:
+ raise errors.Error("Expected a numeric value for {0}".format(name))
+
+
+def _restore_str(unused_name, value):
+ """Restores an string key-value pair from a renewal config file.
+
+ :param str unused_name: option name
+ :param str value: option value
+
+ :returns: converted option value to be stored in the runtime config
+ :rtype: str or None
+
+ """
+ return None if value == "None" else value
def should_renew(config, lineage):
@@ -209,9 +245,6 @@ def should_renew(config, lineage):
def _avoid_invalidating_lineage(config, lineage, original_server):
"Do not renew a valid cert with one from a staging server!"
- def _is_staging(srv):
- return srv == constants.STAGING_URI or "staging" in srv
-
# Some lineages may have begun with --staging, but then had production certs
# added to them
latest_cert = OpenSSL.crypto.load_certificate(
@@ -220,8 +253,8 @@ def _avoid_invalidating_lineage(config, lineage, original_server):
# we should test more methodically
now_valid = "fake" not in repr(latest_cert.get_issuer()).lower()
- if _is_staging(config.server):
- if not _is_staging(original_server) or now_valid:
+ if util.is_staging(config.server):
+ if not util.is_staging(original_server) or now_valid:
if not config.break_my_certs:
names = ", ".join(lineage.names())
raise errors.Error(
@@ -230,12 +263,12 @@ def _avoid_invalidating_lineage(config, lineage, original_server):
"unless you use the --break-my-certs flag!".format(names))
-def renew_cert(config, domains, le_client, lineage):
+def renew_cert(config, le_client, lineage):
"Renew a certificate lineage."
renewal_params = lineage.configuration["renewalparams"]
original_server = renewal_params.get("server", cli.flag_default("server"))
_avoid_invalidating_lineage(config, lineage, original_server)
- new_certr, new_chain, new_key, _ = le_client.obtain_certificate(domains)
+ new_certr, new_chain, new_key, _ = le_client.obtain_certificate(lineage.names())
if config.dry_run:
logger.debug("Dry run: skipping updating lineage at %s",
os.path.dirname(lineage.cert))
@@ -244,12 +277,11 @@ def renew_cert(config, domains, le_client, lineage):
new_cert = OpenSSL.crypto.dump_certificate(
OpenSSL.crypto.FILETYPE_PEM, new_certr.body.wrapped)
new_chain = crypto_util.dump_pyopenssl_chain(new_chain)
- renewal_conf = configuration.RenewerConfiguration(config.namespace)
# TODO: Check return value of save_successor
- lineage.save_successor(prior_version, new_cert, new_key.pem, new_chain, renewal_conf)
+ lineage.save_successor(prior_version, new_cert, new_key.pem, new_chain, config)
lineage.update_all_links_to(lineage.latest_common_version())
- hooks.renew_hook(config, domains, lineage.live_dir)
+ hooks.renew_hook(config, lineage.names(), lineage.live_dir)
def report(msgs, category):
@@ -272,6 +304,9 @@ def _renew_describe_results(config, renew_successes, renew_failures,
notify(report(renew_skipped, "skipped"))
if not renew_successes and not renew_failures:
notify("No renewals were attempted.")
+ if (config.pre_hook is not None or
+ config.renew_hook is not None or config.post_hook is not None):
+ notify("No hooks were run.")
elif renew_successes and not renew_failures:
notify("Congratulations, all renewals succeeded. The following certs "
"have been renewed:")
@@ -300,26 +335,31 @@ def _renew_describe_results(config, renew_successes, renew_failures,
print("\n".join(out))
-def renew_all_lineages(config):
+def handle_renewal_request(config):
"""Examine each lineage; renew if due and report results"""
# This is trivially False if config.domains is empty
if any(domain not in config.webroot_map for domain in config.domains):
# If more plugins start using cli.add_domains,
# we may want to only log a warning here
- raise errors.Error("Currently, the renew verb is only capable of "
+ raise errors.Error("Currently, the renew verb is capable of either "
"renewing all installed certificates that are due "
- "to be renewed; individual domains cannot be "
- "specified with this action. If you would like to "
- "renew specific certificates, use the certonly "
+ "to be renewed or renewing a single certificate specified "
+ "by its name. If you would like to renew specific "
+ "certificates by their domains, use the certonly "
"command. The renew verb may provide other options "
"for selecting certificates to renew in the future.")
- renewer_config = configuration.RenewerConfiguration(config)
+
+ if config.certname:
+ conf_files = [storage.renewal_file_for_certname(config, config.certname)]
+ else:
+ conf_files = storage.renewal_conf_files(config)
+
renew_successes = []
renew_failures = []
renew_skipped = []
parse_failures = []
- for renewal_file in renewal_conf_files(renewer_config):
+ for renewal_file in conf_files:
disp = zope.component.getUtility(interfaces.IDisplay)
disp.notification("Processing " + renewal_file, pause=False)
lineage_config = copy.deepcopy(config)
diff --git a/certbot/reverter.py b/certbot/reverter.py
index 098c74911..32355782e 100644
--- a/certbot/reverter.py
+++ b/certbot/reverter.py
@@ -15,8 +15,6 @@ from certbot import errors
from certbot import interfaces
from certbot import util
-from certbot.display import util as display_util
-
logger = logging.getLogger(__name__)
@@ -183,7 +181,7 @@ class Reverter(object):
if for_logging:
return os.linesep.join(output)
zope.component.getUtility(interfaces.IDisplay).notification(
- os.linesep.join(output), display_util.HEIGHT)
+ os.linesep.join(output), force_interactive=True)
def add_to_temp_checkpoint(self, save_files, save_notes):
"""Add files to temporary checkpoint.
diff --git a/certbot/storage.py b/certbot/storage.py
index c740657d8..af0e9d701 100644
--- a/certbot/storage.py
+++ b/certbot/storage.py
@@ -1,5 +1,6 @@
"""Renewable certificates storage."""
import datetime
+import glob
import logging
import os
import re
@@ -7,6 +8,7 @@ import re
import configobj
import parsedatetime
import pytz
+import shutil
import six
import certbot
@@ -20,9 +22,22 @@ from certbot import util
logger = logging.getLogger(__name__)
ALL_FOUR = ("cert", "privkey", "chain", "fullchain")
+README = "README"
CURRENT_VERSION = util.get_strict_version(certbot.__version__)
+def renewal_conf_files(config):
+ """Return /path/to/*.conf in the renewal conf directory"""
+ return glob.glob(os.path.join(config.renewal_configs_dir, "*.conf"))
+
+def renewal_file_for_certname(config, certname):
+ """Return /path/to/certname.conf in the renewal conf directory"""
+ path = os.path.join(config.renewal_configs_dir, "{0}.conf".format(certname))
+ if not os.path.exists(path):
+ raise errors.CertStorageError("No certificate found with name {0} (expected "
+ "{1}).".format(certname, path))
+ return path
+
def config_with_defaults(config=None):
"""Merge supplied config, if provided, on top of builtin defaults."""
defaults_copy = configobj.ConfigObj(constants.RENEWER_DEFAULTS)
@@ -54,11 +69,12 @@ def add_time_interval(base_time, interval, textparser=parsedatetime.Calendar()):
return textparser.parseDT(interval, base_time, tzinfo=tzinfo)[0]
-def write_renewal_config(o_filename, n_filename, target, relevant_data):
+def write_renewal_config(o_filename, n_filename, archive_dir, target, relevant_data):
"""Writes a renewal config file with the specified name and values.
:param str o_filename: Absolute path to the previous version of config file
:param str n_filename: Absolute path to the new destination of config file
+ :param str archive_dir: Absolute path to the archive directory
:param dict target: Maps ALL_FOUR to their symlink paths
:param dict relevant_data: Renewal configuration options to save
@@ -68,6 +84,7 @@ def write_renewal_config(o_filename, n_filename, target, relevant_data):
"""
config = configobj.ConfigObj(o_filename)
config["version"] = certbot.__version__
+ config["archive_dir"] = archive_dir
for kind in ALL_FOUR:
config[kind] = target[kind]
@@ -94,21 +111,38 @@ def write_renewal_config(o_filename, n_filename, target, relevant_data):
config.write(outfile=f)
return config
+def rename_renewal_config(prev_name, new_name, cli_config):
+ """Renames cli_config.certname's config to cli_config.new_certname.
-def update_configuration(lineagename, target, cli_config):
+ :param .NamespaceConfig cli_config: parsed command line
+ arguments
+ """
+ prev_filename = renewal_filename_for_lineagename(cli_config, prev_name)
+ new_filename = renewal_filename_for_lineagename(cli_config, new_name)
+ if os.path.exists(new_filename):
+ raise errors.ConfigurationError("The new certificate name "
+ "is already in use.")
+ try:
+ os.rename(prev_filename, new_filename)
+ except OSError:
+ raise errors.ConfigurationError("Please specify a valid filename "
+ "for the new certificate name.")
+
+
+def update_configuration(lineagename, archive_dir, target, cli_config):
"""Modifies lineagename's config to contain the specified values.
:param str lineagename: Name of the lineage being modified
+ :param str archive_dir: Absolute path to the archive directory
:param dict target: Maps ALL_FOUR to their symlink paths
- :param .RenewerConfiguration cli_config: parsed command line
+ :param .NamespaceConfig cli_config: parsed command line
arguments
:returns: Configuration object for the updated config file
:rtype: configobj.ConfigObj
"""
- config_filename = os.path.join(
- cli_config.renewal_configs_dir, lineagename) + ".conf"
+ config_filename = renewal_filename_for_lineagename(cli_config, lineagename)
temp_filename = config_filename + ".new"
# If an existing tempfile exists, delete it
@@ -117,7 +151,7 @@ def update_configuration(lineagename, target, cli_config):
# Save only the config items that are relevant to renewal
values = relevant_values(vars(cli_config.namespace))
- write_renewal_config(config_filename, temp_filename, target, values)
+ write_renewal_config(config_filename, temp_filename, archive_dir, target, values)
os.rename(temp_filename, config_filename)
return configobj.ConfigObj(config_filename)
@@ -149,9 +183,9 @@ def _relevant(option):
from certbot import renewal
from certbot.plugins import disco as plugins_disco
plugins = list(plugins_disco.PluginsRegistry.find_all())
- return (option in renewal.STR_CONFIG_ITEMS
- or option in renewal.INT_CONFIG_ITEMS
- or any(option.startswith(x + "_") for x in plugins))
+
+ return (option in renewal.CONFIG_ITEMS or
+ any(option.startswith(x + "_") for x in plugins))
def relevant_values(all_values):
@@ -168,8 +202,109 @@ def relevant_values(all_values):
for option, value in six.iteritems(all_values)
if _relevant(option) and cli.option_was_set(option, value))
+def lineagename_for_filename(config_filename):
+ """Returns the lineagename for a configuration filename.
+ """
+ if not config_filename.endswith(".conf"):
+ raise errors.CertStorageError(
+ "renewal config file name must end in .conf")
+ return os.path.basename(config_filename[:-len(".conf")])
-class RenewableCert(object): # pylint: disable=too-many-instance-attributes
+def renewal_filename_for_lineagename(config, lineagename):
+ """Returns the lineagename for a configuration filename.
+ """
+ return os.path.join(config.renewal_configs_dir, lineagename) + ".conf"
+
+def _relpath_from_file(archive_dir, from_file):
+ """Path to a directory from a file"""
+ return os.path.relpath(archive_dir, os.path.dirname(from_file))
+
+def _full_archive_path(config_obj, cli_config, lineagename):
+ """Returns the full archive path for a lineagename
+
+ Uses cli_config to determine archive path if not available from config_obj.
+
+ :param configobj.ConfigObj config_obj: Renewal conf file contents (can be None)
+ :param configuration.NamespaceConfig cli_config: Main config file
+ :param str lineagename: Certificate name
+ """
+ if config_obj and "archive_dir" in config_obj:
+ return config_obj["archive_dir"]
+ else:
+ return os.path.join(cli_config.default_archive_dir, lineagename)
+
+def _full_live_path(cli_config, lineagename):
+ """Returns the full default live path for a lineagename"""
+ return os.path.join(cli_config.live_dir, lineagename)
+
+def delete_files(config, certname):
+ """Delete all files related to the certificate.
+
+ If some files are not found, ignore them and continue.
+ """
+ renewal_filename = renewal_file_for_certname(config, certname)
+ # file exists
+ full_default_archive_dir = _full_archive_path(None, config, certname)
+ full_default_live_dir = _full_live_path(config, certname)
+ try:
+ renewal_config = configobj.ConfigObj(renewal_filename)
+ except configobj.ConfigObjError:
+ # config is corrupted
+ logger.warning("Could not parse %s. You may wish to manually "
+ "delete the contents of %s and %s.", renewal_filename,
+ full_default_live_dir, full_default_archive_dir)
+ raise errors.CertStorageError(
+ "error parsing {0}".format(renewal_filename))
+ finally:
+ # we couldn't read it, but let's at least delete it
+ # if this was going to fail, it already would have.
+ os.remove(renewal_filename)
+ logger.debug("Removed %s", renewal_filename)
+
+ # cert files and (hopefully) live directory
+ # it's not guaranteed that the files are in our default storage
+ # structure. so, first delete the cert files.
+ directory_names = set()
+ for kind in ALL_FOUR:
+ link = renewal_config.get(kind)
+ try:
+ os.remove(link)
+ logger.debug("Removed %s", link)
+ except OSError:
+ logger.debug("Unable to delete %s", link)
+ directory = os.path.dirname(link)
+ directory_names.add(directory)
+
+ # if all four were in the same directory, and the only thing left
+ # is the README file (or nothing), delete that directory.
+ # this will be wrong in very few but some cases.
+ if len(directory_names) == 1:
+ # delete the README file
+ directory = directory_names.pop()
+ readme_path = os.path.join(directory, README)
+ try:
+ os.remove(readme_path)
+ logger.debug("Removed %s", readme_path)
+ except OSError:
+ logger.debug("Unable to delete %s", readme_path)
+ # if it's now empty, delete the directory
+ try:
+ os.rmdir(directory) # only removes empty directories
+ logger.debug("Removed %s", directory)
+ except OSError:
+ logger.debug("Unable to remove %s; may not be empty.", directory)
+
+ # archive directory
+ try:
+ archive_path = _full_archive_path(renewal_config, config, certname)
+ shutil.rmtree(archive_path)
+ logger.debug("Removed %s", archive_path)
+ except OSError:
+ logger.debug("Unable to remove %s", archive_path)
+
+
+class RenewableCert(object):
+ # pylint: disable=too-many-instance-attributes,too-many-public-methods
"""Renewable certificate.
Represents a lineage of certificates that is under the management of
@@ -204,23 +339,19 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes
renewal configuration file and/or systemwide defaults.
"""
- def __init__(self, config_filename, cli_config):
+ def __init__(self, config_filename, cli_config, update_symlinks=False):
"""Instantiate a RenewableCert object from an existing lineage.
:param str config_filename: the path to the renewal config file
that defines this lineage.
- :param .RenewerConfiguration: parsed command line arguments
+ :param .NamespaceConfig: parsed command line arguments
:raises .CertStorageError: if the configuration file's name didn't end
in ".conf", or the file is missing or broken.
"""
self.cli_config = cli_config
- if not config_filename.endswith(".conf"):
- raise errors.CertStorageError(
- "renewal config file name must end in .conf")
- self.lineagename = os.path.basename(
- config_filename[:-len(".conf")])
+ self.lineagename = lineagename_for_filename(config_filename)
# self.configuration should be used to read parameters that
# may have been chosen based on default values from the
@@ -256,8 +387,41 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes
self.live_dir = os.path.dirname(self.cert)
self._fix_symlinks()
+ if update_symlinks:
+ self._update_symlinks()
self._check_symlinks()
+ @property
+ def target_expiry(self):
+ """The current target certificate's expiration datetime
+
+ :returns: Expiration datetime of the current target certificate
+ :rtype: :class:`datetime.datetime`
+ """
+ return crypto_util.notAfter(self.current_target("cert"))
+
+ @property
+ def archive_dir(self):
+ """Returns the default or specified archive directory"""
+ return _full_archive_path(self.configuration,
+ self.cli_config, self.lineagename)
+
+ def relative_archive_dir(self, from_file):
+ """Returns the default or specified archive directory as a relative path
+
+ Used for creating symbolic links.
+ """
+ return _relpath_from_file(self.archive_dir, from_file)
+
+ @property
+ def is_test_cert(self):
+ """Returns true if this is a test cert from a staging server."""
+ server = self.configuration["renewalparams"].get("server", None)
+ if server:
+ return util.is_staging(server)
+ else:
+ return False
+
def _check_symlinks(self):
"""Raises an exception if a symlink doesn't exist"""
for kind in ALL_FOUR:
@@ -270,6 +434,17 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes
raise errors.CertStorageError("target {0} of symlink {1} does "
"not exist".format(target, link))
+ def _update_symlinks(self):
+ """Updates symlinks to use archive_dir"""
+ for kind in ALL_FOUR:
+ link = getattr(self, kind)
+ previous_link = get_link_target(link)
+ new_link = os.path.join(self.relative_archive_dir(link),
+ os.path.basename(previous_link))
+
+ os.unlink(link)
+ os.symlink(new_link, link)
+
def _consistent(self):
"""Are the files associated with this lineage self-consistent?
@@ -297,16 +472,16 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes
# Each element's link must point within the cert lineage's
# directory within the official archive directory
- desired_directory = os.path.join(
- self.cli_config.archive_dir, self.lineagename)
- if not os.path.samefile(os.path.dirname(target),
- desired_directory):
+ if not os.path.samefile(os.path.dirname(target), self.archive_dir):
logger.debug("Element's link does not point within the "
"cert lineage's directory within the "
"official archive directory. Link: %s, "
"target directory: %s, "
- "archive directory: %s.",
- link, os.path.dirname(target), desired_directory)
+ "archive directory: %s. If you've specified "
+ "the archive directory in the renewal configuration "
+ "file, you may need to update links by running "
+ "certbot update_symlinks.",
+ link, os.path.dirname(target), self.archive_dir)
return False
# The link must point to a file that exists
@@ -647,9 +822,8 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes
if self.has_pending_deployment():
interval = self.configuration.get("deploy_before_expiry",
"5 days")
- expiry = crypto_util.notAfter(self.current_target("cert"))
now = pytz.UTC.fromutc(datetime.datetime.utcnow())
- if expiry < add_time_interval(now, interval):
+ if self.target_expiry < add_time_interval(now, interval):
return True
return False
@@ -750,7 +924,7 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes
:param str cert: the initial certificate version in PEM format
:param str privkey: the private key in PEM format
:param str chain: the certificate chain in PEM format
- :param .RenewerConfiguration cli_config: parsed command line
+ :param .NamespaceConfig cli_config: parsed command line
arguments
:returns: the newly-created RenewalCert object
@@ -759,23 +933,20 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes
"""
# Examine the configuration and find the new lineage's name
- for i in (cli_config.renewal_configs_dir, cli_config.archive_dir,
+ for i in (cli_config.renewal_configs_dir, cli_config.default_archive_dir,
cli_config.live_dir):
if not os.path.exists(i):
os.makedirs(i, 0o700)
logger.debug("Creating directory %s.", i)
config_file, config_filename = util.unique_lineage_name(
cli_config.renewal_configs_dir, lineagename)
- if not config_filename.endswith(".conf"):
- raise errors.CertStorageError(
- "renewal config file name must end in .conf")
# Determine where on disk everything will go
# lineagename will now potentially be modified based on which
# renewal configuration file could actually be created
- lineagename = os.path.basename(config_filename)[:-len(".conf")]
- archive = os.path.join(cli_config.archive_dir, lineagename)
- live_dir = os.path.join(cli_config.live_dir, lineagename)
+ lineagename = lineagename_for_filename(config_filename)
+ archive = _full_archive_path(None, cli_config, lineagename)
+ live_dir = _full_live_path(cli_config, lineagename)
if os.path.exists(archive):
raise errors.CertStorageError(
"archive directory exists for " + lineagename)
@@ -786,37 +957,52 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes
os.mkdir(live_dir)
logger.debug("Archive directory %s and live "
"directory %s created.", archive, live_dir)
- relative_archive = os.path.join("..", "..", "archive", lineagename)
# Put the data into the appropriate files on disk
target = dict([(kind, os.path.join(live_dir, kind + ".pem"))
for kind in ALL_FOUR])
for kind in ALL_FOUR:
- os.symlink(os.path.join(relative_archive, kind + "1.pem"),
+ os.symlink(os.path.join(_relpath_from_file(archive, target[kind]), kind + "1.pem"),
target[kind])
- with open(target["cert"], "w") as f:
+ with open(target["cert"], "wb") as f:
logger.debug("Writing certificate to %s.", target["cert"])
f.write(cert)
- with open(target["privkey"], "w") as f:
+ with open(target["privkey"], "wb") as f:
logger.debug("Writing private key to %s.", target["privkey"])
f.write(privkey)
# XXX: Let's make sure to get the file permissions right here
- with open(target["chain"], "w") as f:
+ with open(target["chain"], "wb") as f:
logger.debug("Writing chain to %s.", target["chain"])
f.write(chain)
- with open(target["fullchain"], "w") as f:
+ with open(target["fullchain"], "wb") as f:
# assumes that OpenSSL.crypto.dump_certificate includes
# ending newline character
logger.debug("Writing full chain to %s.", target["fullchain"])
f.write(cert + chain)
+ # Write a README file to the live directory
+ readme_path = os.path.join(live_dir, README)
+ with open(readme_path, "w") as f:
+ logger.debug("Writing README to %s.", readme_path)
+ f.write("This directory contains your keys and certificates.\n\n"
+ "`privkey.pem` : the private key for your certificate.\n"
+ "`fullchain.pem`: the certificate file used in most server software.\n"
+ "`chain.pem` : used for OCSP stapling in Nginx >=1.3.7.\n"
+ "`cert.pem` : will break many server configurations, and "
+ "should not be used\n"
+ " without reading further documentation (see link below).\n\n"
+ "We recommend not moving these files. For more information, see the Certbot\n"
+ "User Guide at https://certbot.eff.org/docs/using.html#where-are-my-"
+ "certificates.\n")
+
# Document what we've done in a new renewal config file
config_file.close()
# Save only the config items that are relevant to renewal
values = relevant_values(vars(cli_config.namespace))
- new_config = write_renewal_config(config_filename, config_filename, target, values)
+ new_config = write_renewal_config(config_filename, config_filename, archive,
+ target, values)
return cls(new_config.filename, cli_config)
def save_successor(self, prior_version, new_cert,
@@ -836,7 +1022,7 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes
:param str new_privkey: the new private key, in PEM format,
or ``None``, if the private key has not changed
:param str new_chain: the new chain, in PEM format
- :param .RenewerConfiguration cli_config: parsed command line
+ :param .NamespaceConfig cli_config: parsed command line
arguments
:returns: the new version number that was created
@@ -851,14 +1037,9 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes
self.cli_config = cli_config
target_version = self.next_free_version()
- archive = self.cli_config.archive_dir
- # XXX if anyone ever moves a renewal configuration file, this will
- # break... perhaps prefix should be the dirname of the previous
- # cert.pem?
- prefix = os.path.join(archive, self.lineagename)
target = dict(
[(kind,
- os.path.join(prefix, "{0}{1}.pem".format(kind, target_version)))
+ os.path.join(self.archive_dir, "{0}{1}.pem".format(kind, target_version)))
for kind in ALL_FOUR])
# Distinguish the cases where the privkey has changed and where it
@@ -868,7 +1049,7 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes
# The behavior below keeps the prior key by creating a new
# symlink to the old key or the target of the old key symlink.
old_privkey = os.path.join(
- prefix, "privkey{0}.pem".format(prior_version))
+ self.archive_dir, "privkey{0}.pem".format(prior_version))
if os.path.islink(old_privkey):
old_privkey = os.readlink(old_privkey)
else:
@@ -894,7 +1075,7 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes
symlinks = dict((kind, self.configuration[kind]) for kind in ALL_FOUR)
# Update renewal config file
self.configfile = update_configuration(
- self.lineagename, symlinks, cli_config)
+ self.lineagename, self.archive_dir, symlinks, cli_config)
self.configuration = config_with_defaults(self.configfile)
return target_version
diff --git a/certbot/tests/account_test.py b/certbot/tests/account_test.py
index 41b835838..1c50025d7 100644
--- a/certbot/tests/account_test.py
+++ b/certbot/tests/account_test.py
@@ -14,10 +14,10 @@ from acme import messages
from certbot import errors
-from certbot.tests import test_util
+from certbot.tests import util
-KEY = jose.JWKRSA.load(test_util.load_vector("rsa512_key_2.pem"))
+KEY = jose.JWKRSA.load(util.load_vector("rsa512_key_2.pem"))
class AccountTest(unittest.TestCase):
diff --git a/certbot/tests/acme_util.py b/certbot/tests/acme_util.py
index de64dfef9..5e6b190a7 100644
--- a/certbot/tests/acme_util.py
+++ b/certbot/tests/acme_util.py
@@ -7,10 +7,13 @@ from acme import challenges
from acme import jose
from acme import messages
-from certbot.tests import test_util
+from certbot import auth_handler
+
+from certbot.tests import util
-KEY = test_util.load_rsa_private_key('rsa512_key.pem')
+JWK = jose.JWK.load(util.load_vector('rsa512_key.pem'))
+KEY = util.load_rsa_private_key('rsa512_key.pem')
# Challenges
HTTP01 = challenges.HTTP01(
@@ -50,6 +53,14 @@ DNS01_P = chall_to_challb(DNS01, messages.STATUS_PENDING)
CHALLENGES_P = [HTTP01_P, TLSSNI01_P, DNS01_P]
+# AnnotatedChallenge objects
+HTTP01_A = auth_handler.challb_to_achall(HTTP01_P, JWK, "example.com")
+TLSSNI01_A = auth_handler.challb_to_achall(TLSSNI01_P, JWK, "example.net")
+DNS01_A = auth_handler.challb_to_achall(DNS01_P, JWK, "example.org")
+
+ACHALLENGES = [HTTP01_A, TLSSNI01_A, DNS01_A]
+
+
def gen_authzr(authz_status, domain, challs, statuses, combos=True):
"""Generate an authorization resource.
diff --git a/certbot/tests/auth_handler_test.py b/certbot/tests/auth_handler_test.py
index 9e0add196..441550fc8 100644
--- a/certbot/tests/auth_handler_test.py
+++ b/certbot/tests/auth_handler_test.py
@@ -15,6 +15,7 @@ from certbot import errors
from certbot import util
from certbot.tests import acme_util
+from certbot.tests import util as test_util
class ChallengeFactoryTest(unittest.TestCase):
@@ -251,7 +252,7 @@ class PollChallengesTest(unittest.TestCase):
self.assertEqual(authzr.body.status, messages.STATUS_PENDING)
@mock.patch("certbot.auth_handler.time")
- @mock.patch("certbot.auth_handler.zope.component.getUtility")
+ @test_util.patch_get_utility()
def test_poll_challenges_failure(self, unused_mock_time, unused_mock_zope):
self.mock_net.poll.side_effect = self._mock_poll_solve_one_invalid
self.assertRaises(
@@ -412,7 +413,7 @@ class ReportFailedChallsTest(unittest.TestCase):
domain="foo.bar",
account_key="key")
- @mock.patch("certbot.auth_handler.zope.component.getUtility")
+ @test_util.patch_get_utility()
def test_same_error_and_domain(self, mock_zope):
from certbot import auth_handler
@@ -421,7 +422,7 @@ class ReportFailedChallsTest(unittest.TestCase):
self.assertTrue(len(call_list) == 1)
self.assertTrue("Domain: example.com\nType: tls\nDetail: detail" in call_list[0][0][0])
- @mock.patch("certbot.auth_handler.zope.component.getUtility")
+ @test_util.patch_get_utility()
def test_different_errors_and_domains(self, mock_zope):
from certbot import auth_handler
diff --git a/certbot/tests/cert_manager_test.py b/certbot/tests/cert_manager_test.py
new file mode 100644
index 000000000..d7d1a3aff
--- /dev/null
+++ b/certbot/tests/cert_manager_test.py
@@ -0,0 +1,472 @@
+"""Tests for certbot.cert_manager."""
+# pylint: disable=protected-access
+import os
+import re
+import shutil
+import tempfile
+import unittest
+
+import configobj
+import mock
+
+from certbot import configuration
+from certbot import errors
+
+from certbot.display import util as display_util
+from certbot.storage import ALL_FOUR
+
+from certbot.tests import storage_test
+from certbot.tests import util as test_util
+
+class BaseCertManagerTest(unittest.TestCase):
+ """Base class for setting up Cert Manager tests.
+ """
+ def setUp(self):
+ self.tempdir = tempfile.mkdtemp()
+
+ os.makedirs(os.path.join(self.tempdir, "renewal"))
+
+ self.cli_config = configuration.NamespaceConfig(mock.MagicMock(
+ config_dir=self.tempdir,
+ work_dir=self.tempdir,
+ logs_dir=self.tempdir,
+ quiet=False,
+ ))
+
+ self.domains = {
+ "example.org": None,
+ "other.com": os.path.join(self.tempdir, "specialarchive")
+ }
+ self.configs = dict((domain, self._set_up_config(domain, self.domains[domain]))
+ for domain in self.domains)
+
+ # We also create a file that isn't a renewal config in the same
+ # location to test that logic that reads in all-and-only renewal
+ # configs will ignore it and NOT attempt to parse it.
+ junk = open(os.path.join(self.tempdir, "renewal", "IGNORE.THIS"), "w")
+ junk.write("This file should be ignored!")
+ junk.close()
+
+ def _set_up_config(self, domain, custom_archive):
+ # TODO: maybe provide NamespaceConfig.make_dirs?
+ # TODO: main() should create those dirs, c.f. #902
+ os.makedirs(os.path.join(self.tempdir, "live", domain))
+ config = configobj.ConfigObj()
+
+ if custom_archive is not None:
+ os.makedirs(custom_archive)
+ config["archive_dir"] = custom_archive
+ else:
+ os.makedirs(os.path.join(self.tempdir, "archive", domain))
+
+ for kind in ALL_FOUR:
+ config[kind] = os.path.join(self.tempdir, "live", domain,
+ kind + ".pem")
+
+ config.filename = os.path.join(self.tempdir, "renewal",
+ domain + ".conf")
+ config.write()
+ return config
+
+ def tearDown(self):
+ shutil.rmtree(self.tempdir)
+
+
+class UpdateLiveSymlinksTest(BaseCertManagerTest):
+ """Tests for certbot.cert_manager.update_live_symlinks
+ """
+ def test_update_live_symlinks(self):
+ """Test update_live_symlinks"""
+ # pylint: disable=too-many-statements
+ # create files with incorrect symlinks
+ from certbot import cert_manager
+ archive_paths = {}
+ for domain in self.domains:
+ custom_archive = self.domains[domain]
+ if custom_archive is not None:
+ archive_dir_path = custom_archive
+ else:
+ archive_dir_path = os.path.join(self.tempdir, "archive", domain)
+ archive_paths[domain] = dict((kind,
+ os.path.join(archive_dir_path, kind + "1.pem")) for kind in ALL_FOUR)
+ for kind in ALL_FOUR:
+ live_path = self.configs[domain][kind]
+ archive_path = archive_paths[domain][kind]
+ open(archive_path, 'a').close()
+ # path is incorrect but base must be correct
+ os.symlink(os.path.join(self.tempdir, kind + "1.pem"), live_path)
+
+ # run update symlinks
+ cert_manager.update_live_symlinks(self.cli_config)
+
+ # check that symlinks go where they should
+ prev_dir = os.getcwd()
+ try:
+ for domain in self.domains:
+ for kind in ALL_FOUR:
+ os.chdir(os.path.dirname(self.configs[domain][kind]))
+ self.assertEqual(
+ os.path.realpath(os.readlink(self.configs[domain][kind])),
+ os.path.realpath(archive_paths[domain][kind]))
+ finally:
+ os.chdir(prev_dir)
+
+
+class DeleteTest(storage_test.BaseRenewableCertTest):
+ """Tests for certbot.cert_manager.delete
+ """
+ @test_util.patch_get_utility()
+ @mock.patch('certbot.cert_manager.lineage_for_certname')
+ @mock.patch('certbot.storage.delete_files')
+ def test_delete(self, mock_delete_files, mock_lineage_for_certname, unused_get_utility):
+ """Test delete"""
+ mock_lineage_for_certname.return_value = self.test_rc
+ self.cli_config.certname = "example.org"
+ from certbot import cert_manager
+ cert_manager.delete(self.cli_config)
+ self.assertTrue(mock_delete_files.called)
+
+
+class CertificatesTest(BaseCertManagerTest):
+ """Tests for certbot.cert_manager.certificates
+ """
+ def _certificates(self, *args, **kwargs):
+ from certbot.cert_manager import certificates
+ return certificates(*args, **kwargs)
+
+ @mock.patch('certbot.cert_manager.logger')
+ @test_util.patch_get_utility()
+ def test_certificates_parse_fail(self, mock_utility, mock_logger):
+ self._certificates(self.cli_config)
+ self.assertTrue(mock_logger.warning.called) #pylint: disable=no-member
+ self.assertTrue(mock_utility.called)
+
+ @mock.patch('certbot.cert_manager.logger')
+ @test_util.patch_get_utility()
+ def test_certificates_quiet(self, mock_utility, mock_logger):
+ self.cli_config.quiet = True
+ self._certificates(self.cli_config)
+ self.assertFalse(mock_utility.notification.called)
+ self.assertTrue(mock_logger.warning.called) #pylint: disable=no-member
+
+ @mock.patch('certbot.cert_manager.logger')
+ @test_util.patch_get_utility()
+ @mock.patch("certbot.storage.RenewableCert")
+ @mock.patch('certbot.cert_manager._report_human_readable')
+ def test_certificates_parse_success(self, mock_report, mock_renewable_cert,
+ mock_utility, mock_logger):
+ mock_report.return_value = ""
+ self._certificates(self.cli_config)
+ self.assertFalse(mock_logger.warning.called) #pylint: disable=no-member
+ self.assertTrue(mock_report.called)
+ self.assertTrue(mock_utility.called)
+ self.assertTrue(mock_renewable_cert.called)
+
+ @mock.patch('certbot.cert_manager.logger')
+ @test_util.patch_get_utility()
+ def test_certificates_no_files(self, mock_utility, mock_logger):
+ tempdir = tempfile.mkdtemp()
+
+ cli_config = configuration.NamespaceConfig(mock.MagicMock(
+ config_dir=tempdir,
+ work_dir=tempdir,
+ logs_dir=tempdir,
+ quiet=False,
+ ))
+
+ os.makedirs(os.path.join(tempdir, "renewal"))
+ self._certificates(cli_config)
+ self.assertFalse(mock_logger.warning.called) #pylint: disable=no-member
+ self.assertTrue(mock_utility.called)
+ shutil.rmtree(tempdir)
+
+ @mock.patch('certbot.cert_manager.ocsp.RevocationChecker.ocsp_revoked')
+ def test_report_human_readable(self, mock_revoked):
+ mock_revoked.return_value = None
+ from certbot import cert_manager
+ import datetime, pytz
+ expiry = pytz.UTC.fromutc(datetime.datetime.utcnow())
+
+ cert = mock.MagicMock(lineagename="nameone")
+ cert.target_expiry = expiry
+ cert.names.return_value = ["nameone", "nametwo"]
+ cert.is_test_cert = False
+ parsed_certs = [cert]
+
+ # pylint: disable=protected-access
+ get_report = lambda: cert_manager._report_human_readable(mock_config, parsed_certs)
+
+ mock_config = mock.MagicMock(certname=None, lineagename=None)
+ # pylint: disable=protected-access
+ out = get_report()
+ self.assertTrue("INVALID: EXPIRED" in out)
+
+ cert.target_expiry += datetime.timedelta(hours=2)
+ # pylint: disable=protected-access
+ out = get_report()
+ self.assertTrue('1 hour(s)' in out)
+ self.assertTrue('VALID' in out and not 'INVALID' in out)
+
+ cert.target_expiry += datetime.timedelta(days=1)
+ # pylint: disable=protected-access
+ out = get_report()
+ self.assertTrue('1 day' in out)
+ self.assertFalse('under' in out)
+ self.assertTrue('VALID' in out and not 'INVALID' in out)
+
+ cert.target_expiry += datetime.timedelta(days=2)
+ # pylint: disable=protected-access
+ out = get_report()
+ self.assertTrue('3 days' in out)
+ self.assertTrue('VALID' in out and not 'INVALID' in out)
+
+ cert.is_test_cert = True
+ mock_revoked.return_value = True
+ out = get_report()
+ self.assertTrue('INVALID: TEST_CERT, REVOKED' in out)
+
+ cert = mock.MagicMock(lineagename="indescribable")
+ cert.target_expiry = expiry
+ cert.names.return_value = ["nameone", "thrice.named"]
+ cert.is_test_cert = True
+ parsed_certs.append(cert)
+
+ out = get_report()
+ self.assertEqual(len(re.findall("INVALID:", out)), 2)
+ mock_config.domains = ["thrice.named"]
+ out = get_report()
+ self.assertEqual(len(re.findall("INVALID:", out)), 1)
+ mock_config.domains = ["nameone"]
+ out = get_report()
+ self.assertEqual(len(re.findall("INVALID:", out)), 2)
+ mock_config.certname = "indescribable"
+ out = get_report()
+ self.assertEqual(len(re.findall("INVALID:", out)), 1)
+ mock_config.certname = "horror"
+ out = get_report()
+ self.assertEqual(len(re.findall("INVALID:", out)), 0)
+
+
+class SearchLineagesTest(BaseCertManagerTest):
+ """Tests for certbot.cert_manager._search_lineages."""
+
+ @mock.patch('certbot.util.make_or_verify_dir')
+ @mock.patch('certbot.storage.renewal_conf_files')
+ @mock.patch('certbot.storage.RenewableCert')
+ def test_cert_storage_error(self, mock_renewable_cert, mock_renewal_conf_files,
+ mock_make_or_verify_dir):
+ mock_renewal_conf_files.return_value = ["badfile"]
+ mock_renewable_cert.side_effect = errors.CertStorageError
+ from certbot import cert_manager
+ # pylint: disable=protected-access
+ self.assertEqual(cert_manager._search_lineages(self.cli_config, lambda x: x, "check"),
+ "check")
+ self.assertTrue(mock_make_or_verify_dir.called)
+
+
+class LineageForCertnameTest(BaseCertManagerTest):
+ """Tests for certbot.cert_manager.lineage_for_certname"""
+
+ @mock.patch('certbot.util.make_or_verify_dir')
+ @mock.patch('certbot.storage.renewal_conf_files')
+ @mock.patch('certbot.storage.RenewableCert')
+ def test_found_match(self, mock_renewable_cert, mock_renewal_conf_files,
+ mock_make_or_verify_dir):
+ mock_renewal_conf_files.return_value = ["somefile.conf"]
+ mock_match = mock.Mock(lineagename="example.com")
+ mock_renewable_cert.return_value = mock_match
+ from certbot import cert_manager
+ self.assertEqual(cert_manager.lineage_for_certname(self.cli_config, "example.com"),
+ mock_match)
+ self.assertTrue(mock_make_or_verify_dir.called)
+
+ @mock.patch('certbot.util.make_or_verify_dir')
+ @mock.patch('certbot.storage.renewal_conf_files')
+ @mock.patch('certbot.storage.RenewableCert')
+ def test_no_match(self, mock_renewable_cert, mock_renewal_conf_files,
+ mock_make_or_verify_dir):
+ mock_renewal_conf_files.return_value = ["somefile.conf"]
+ mock_match = mock.Mock(lineagename="other.com")
+ mock_renewable_cert.return_value = mock_match
+ from certbot import cert_manager
+ self.assertEqual(cert_manager.lineage_for_certname(self.cli_config, "example.com"),
+ None)
+ self.assertTrue(mock_make_or_verify_dir.called)
+
+
+class DomainsForCertnameTest(BaseCertManagerTest):
+ """Tests for certbot.cert_manager.domains_for_certname"""
+
+ @mock.patch('certbot.util.make_or_verify_dir')
+ @mock.patch('certbot.storage.renewal_conf_files')
+ @mock.patch('certbot.storage.RenewableCert')
+ def test_found_match(self, mock_renewable_cert, mock_renewal_conf_files,
+ mock_make_or_verify_dir):
+ mock_renewal_conf_files.return_value = ["somefile.conf"]
+ mock_match = mock.Mock(lineagename="example.com")
+ domains = ["example.com", "example.org"]
+ mock_match.names.return_value = domains
+ mock_renewable_cert.return_value = mock_match
+ from certbot import cert_manager
+ self.assertEqual(cert_manager.domains_for_certname(self.cli_config, "example.com"),
+ domains)
+ self.assertTrue(mock_make_or_verify_dir.called)
+
+ @mock.patch('certbot.util.make_or_verify_dir')
+ @mock.patch('certbot.storage.renewal_conf_files')
+ @mock.patch('certbot.storage.RenewableCert')
+ def test_no_match(self, mock_renewable_cert, mock_renewal_conf_files,
+ mock_make_or_verify_dir):
+ mock_renewal_conf_files.return_value = ["somefile.conf"]
+ mock_match = mock.Mock(lineagename="example.com")
+ domains = ["example.com", "example.org"]
+ mock_match.names.return_value = domains
+ mock_renewable_cert.return_value = mock_match
+ from certbot import cert_manager
+ self.assertEqual(cert_manager.domains_for_certname(self.cli_config, "other.com"),
+ None)
+ self.assertTrue(mock_make_or_verify_dir.called)
+
+
+class RenameLineageTest(BaseCertManagerTest):
+ """Tests for certbot.cert_manager.rename_lineage"""
+
+ def setUp(self):
+ super(RenameLineageTest, self).setUp()
+ self.mock_config = configuration.NamespaceConfig(
+ namespace=mock.MagicMock(
+ config_dir=self.tempdir,
+ work_dir=self.tempdir,
+ logs_dir=self.tempdir,
+ certname="example.org",
+ new_certname="after",
+ )
+ )
+
+ def _call(self, *args, **kwargs):
+ from certbot import cert_manager
+ return cert_manager.rename_lineage(*args, **kwargs)
+
+ @mock.patch('certbot.storage.renewal_conf_files')
+ @test_util.patch_get_utility()
+ def test_no_certname(self, mock_get_utility, mock_renewal_conf_files):
+ mock_config = mock.Mock(certname=None, new_certname="two")
+
+ # if not choices
+ mock_renewal_conf_files.return_value = []
+ self.assertRaises(errors.Error, self._call, mock_config)
+
+ mock_renewal_conf_files.return_value = ["one.conf"]
+ util_mock = mock.Mock()
+ util_mock.menu.return_value = (display_util.CANCEL, 0)
+ mock_get_utility.return_value = util_mock
+ self.assertRaises(errors.Error, self._call, mock_config)
+
+ util_mock.menu.return_value = (display_util.OK, -1)
+ self.assertRaises(errors.Error, self._call, mock_config)
+
+ @test_util.patch_get_utility()
+ def test_no_new_certname(self, mock_get_utility):
+ mock_config = mock.Mock(certname="one", new_certname=None)
+
+ util_mock = mock.Mock()
+ util_mock.input.return_value = (display_util.CANCEL, "name")
+ mock_get_utility.return_value = util_mock
+ self.assertRaises(errors.Error, self._call, mock_config)
+
+ util_mock = mock.Mock()
+ util_mock.input.return_value = (display_util.OK, None)
+ mock_get_utility.return_value = util_mock
+ self.assertRaises(errors.Error, self._call, mock_config)
+
+ @test_util.patch_get_utility()
+ @mock.patch('certbot.cert_manager.lineage_for_certname')
+ def test_no_existing_certname(self, mock_lineage_for_certname, unused_get_utility):
+ mock_config = mock.Mock(certname="one", new_certname="two")
+ mock_lineage_for_certname.return_value = None
+ self.assertRaises(errors.ConfigurationError,
+ self._call, mock_config)
+
+ @test_util.patch_get_utility()
+ @mock.patch("certbot.storage.RenewableCert._check_symlinks")
+ def test_rename_cert(self, mock_check, unused_get_utility):
+ mock_check.return_value = True
+ mock_config = self.mock_config
+ self._call(mock_config)
+ from certbot import cert_manager
+ updated_lineage = cert_manager.lineage_for_certname(mock_config, mock_config.new_certname)
+ self.assertTrue(updated_lineage is not None)
+ self.assertEqual(updated_lineage.lineagename, mock_config.new_certname)
+
+ @test_util.patch_get_utility()
+ @mock.patch("certbot.storage.RenewableCert._check_symlinks")
+ def test_rename_cert_interactive_certname(self, mock_check, mock_get_utility):
+ mock_check.return_value = True
+ mock_config = self.mock_config
+ mock_config.certname = None
+ util_mock = mock.Mock()
+ util_mock.menu.return_value = (display_util.OK, 0)
+ mock_get_utility.return_value = util_mock
+ self._call(mock_config)
+ from certbot import cert_manager
+ updated_lineage = cert_manager.lineage_for_certname(mock_config, mock_config.new_certname)
+ self.assertTrue(updated_lineage is not None)
+ self.assertEqual(updated_lineage.lineagename, mock_config.new_certname)
+
+ @test_util.patch_get_utility()
+ @mock.patch("certbot.storage.RenewableCert._check_symlinks")
+ def test_rename_cert_bad_new_certname(self, mock_check, unused_get_utility):
+ mock_check.return_value = True
+ mock_config = self.mock_config
+
+ # for example, don't rename to existing certname
+ mock_config.new_certname = "example.org"
+ self.assertRaises(errors.ConfigurationError, self._call, mock_config)
+
+ mock_config.new_certname = "one{0}two".format(os.path.sep)
+ self.assertRaises(errors.ConfigurationError, self._call, mock_config)
+
+
+class DuplicativeCertsTest(storage_test.BaseRenewableCertTest):
+ """Test to avoid duplicate lineages."""
+
+ def setUp(self):
+ super(DuplicativeCertsTest, self).setUp()
+ self.config.write()
+ self._write_out_ex_kinds()
+
+ def tearDown(self):
+ shutil.rmtree(self.tempdir)
+
+ @mock.patch('certbot.util.make_or_verify_dir')
+ def test_find_duplicative_names(self, unused_makedir):
+ from certbot.cert_manager import find_duplicative_certs
+ test_cert = test_util.load_vector('cert-san.pem')
+ with open(self.test_rc.cert, 'wb') as f:
+ f.write(test_cert)
+
+ # No overlap at all
+ result = find_duplicative_certs(
+ self.cli_config, ['wow.net', 'hooray.org'])
+ self.assertEqual(result, (None, None))
+
+ # Totally identical
+ result = find_duplicative_certs(
+ self.cli_config, ['example.com', 'www.example.com'])
+ self.assertTrue(result[0].configfile.filename.endswith('example.org.conf'))
+ self.assertEqual(result[1], None)
+
+ # Superset
+ result = find_duplicative_certs(
+ self.cli_config, ['example.com', 'www.example.com', 'something.new'])
+ self.assertEqual(result[0], None)
+ self.assertTrue(result[1].configfile.filename.endswith('example.org.conf'))
+
+ # Partial overlap doesn't count
+ result = find_duplicative_certs(
+ self.cli_config, ['example.com', 'something.new'])
+ self.assertEqual(result, (None, None))
+
+
+if __name__ == "__main__":
+ unittest.main() # pragma: no cover
diff --git a/certbot/tests/cli_test.py b/certbot/tests/cli_test.py
index 5d2051cfe..32aada811 100644
--- a/certbot/tests/cli_test.py
+++ b/certbot/tests/cli_test.py
@@ -1,120 +1,108 @@
"""Tests for certbot.cli."""
-# Many tests in this file should be moved into
-# main_test.py and renewal_test.py. See #2716.
-# pylint: disable=too-many-lines
-from __future__ import print_function
-
import argparse
-import dialog
import functools
-import itertools
-import os
-import shutil
-import traceback
-import tempfile
import unittest
+import os
+import tempfile
-import mock
import six
+import mock
from six.moves import reload_module # pylint: disable=import-error
-from acme import jose
-
-from certbot import account
from certbot import cli
-from certbot import configuration
from certbot import constants
-from certbot import crypto_util
from certbot import errors
-from certbot import util
-from certbot import main
-from certbot import renewal
-from certbot import storage
-
from certbot.plugins import disco
-from certbot.plugins import manual
-from certbot.tests import storage_test
-from certbot.tests import test_util
+def reset_set_by_cli():
+ '''Reset the state of the `set_by_cli` function'''
+ cli.set_by_cli.detector = None
+
+class TestReadFile(unittest.TestCase):
+ '''Test cli.read_file'''
+
+ _multiprocess_can_split_ = True
+
+ def test_read_file(self):
+ tmp_dir = tempfile.mkdtemp()
+ rel_test_path = os.path.relpath(os.path.join(tmp_dir, 'foo'))
+ self.assertRaises(
+ argparse.ArgumentTypeError, cli.read_file, rel_test_path)
+
+ test_contents = b'bar\n'
+ with open(rel_test_path, 'wb') as f:
+ f.write(test_contents)
+
+ path, contents = cli.read_file(rel_test_path)
+ self.assertEqual(path, os.path.abspath(path))
+ self.assertEqual(contents, test_contents)
-CERT = test_util.vector_path('cert.pem')
-CSR = test_util.vector_path('csr.der')
-KEY = test_util.vector_path('rsa256_key.pem')
+class ParseTest(unittest.TestCase):
+ '''Test the cli args entrypoint'''
+ _multiprocess_can_split_ = True
-class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods
- """Tests for different commands."""
+ @classmethod
+ def setUpClass(cls):
+ cls.plugins = disco.PluginsRegistry.find_all()
+ cls.parse = functools.partial(cli.prepare_and_parse_args, cls.plugins)
def setUp(self):
- self.tmp_dir = tempfile.mkdtemp()
- self.config_dir = os.path.join(self.tmp_dir, 'config')
- self.work_dir = os.path.join(self.tmp_dir, 'work')
- self.logs_dir = os.path.join(self.tmp_dir, 'logs')
- self.standard_args = ['--config-dir', self.config_dir,
- '--work-dir', self.work_dir,
- '--logs-dir', self.logs_dir, '--text']
-
- def tearDown(self):
- shutil.rmtree(self.tmp_dir)
- # Reset globals in cli
- # pylint: disable=protected-access
- cli._parser = cli.set_by_cli.detector = None
-
- def _call(self, args, stdout=None):
- "Run the cli with output streams and actual client mocked out"
- with mock.patch('certbot.main.client') as client:
- ret, stdout, stderr = self._call_no_clientmock(args, stdout)
- return ret, stdout, stderr, client
-
- def _call_no_clientmock(self, args, stdout=None):
- "Run the client with output streams mocked out"
- args = self.standard_args + args
-
- toy_stdout = stdout if stdout else six.StringIO()
- with mock.patch('certbot.main.sys.stdout', new=toy_stdout):
- with mock.patch('certbot.main.sys.stderr') as stderr:
- ret = main.main(args[:]) # NOTE: parser can alter its args!
- return ret, toy_stdout, stderr
-
- def test_no_flags(self):
- with mock.patch('certbot.main.run') as mock_run:
- self._call([])
- self.assertEqual(1, mock_run.call_count)
+ reset_set_by_cli()
def _help_output(self, args):
"Run a command, and return the ouput string for scrutiny"
output = six.StringIO()
- self.assertRaises(SystemExit, self._call, args, output)
- out = output.getvalue()
- return out
+ with mock.patch('certbot.main.sys.stdout', new=output):
+ with mock.patch('certbot.main.sys.stderr'):
+ self.assertRaises(SystemExit, self.parse, args, output)
+ return output.getvalue()
+
+ def test_install_abspath(self):
+ cert = 'cert'
+ key = 'key'
+ chain = 'chain'
+ fullchain = 'fullchain'
+
+ with mock.patch('certbot.main.install'):
+ namespace = self.parse(['install', '--cert-path', cert,
+ '--key-path', 'key', '--chain-path',
+ 'chain', '--fullchain-path', 'fullchain'])
+
+ self.assertEqual(namespace.cert_path, os.path.abspath(cert))
+ self.assertEqual(namespace.key_path, os.path.abspath(key))
+ self.assertEqual(namespace.chain_path, os.path.abspath(chain))
+ self.assertEqual(namespace.fullchain_path, os.path.abspath(fullchain))
def test_help(self):
- self.assertRaises(SystemExit, self._call, ['--help'])
- self.assertRaises(SystemExit, self._call, ['--help', 'all'])
- plugins = disco.PluginsRegistry.find_all()
+ self._help_output(['--help']) # assert SystemExit is raised here
out = self._help_output(['--help', 'all'])
self.assertTrue("--configurator" in out)
self.assertTrue("how a cert is deployed" in out)
- self.assertTrue("--manual-test-mode" in out)
+ self.assertTrue("--webroot-path" in out)
+ self.assertTrue("--text" not in out)
+ self.assertTrue("--dialog" not in out)
+ self.assertTrue("%s" not in out)
+ self.assertTrue("{0}" not in out)
out = self._help_output(['-h', 'nginx'])
- if "nginx" in plugins:
+ if "nginx" in self.plugins:
# may be false while building distributions without plugins
self.assertTrue("--nginx-ctl" in out)
- self.assertTrue("--manual-test-mode" not in out)
+ self.assertTrue("--webroot-path" not in out)
self.assertTrue("--checkpoints" not in out)
out = self._help_output(['-h'])
- self.assertTrue("letsencrypt-auto" not in out) # test cli.cli_command
- if "nginx" in plugins:
+ self.assertTrue("letsencrypt-auto" not in out) # test cli.cli_command
+ if "nginx" in self.plugins:
self.assertTrue("Use the Nginx plugin" in out)
else:
- self.assertTrue("(nginx support is experimental" in out)
+ self.assertTrue("(the certbot nginx plugin is not" in out)
out = self._help_output(['--help', 'plugins'])
- self.assertTrue("--manual-test-mode" not in out)
+ self.assertTrue("--webroot-path" not in out)
self.assertTrue("--prepare" in out)
self.assertTrue('"plugins" subcommand' in out)
@@ -133,321 +121,77 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods
out = self._help_output(['--help', 'revoke'])
self.assertTrue("--cert-path" in out)
self.assertTrue("--key-path" in out)
+ self.assertTrue("--reason" in out)
out = self._help_output(['-h', 'config_changes'])
self.assertTrue("--cert-path" not in out)
self.assertTrue("--key-path" not in out)
out = self._help_output(['-h'])
- self.assertTrue(cli.usage_strings(plugins)[0] in out)
-
- def _cli_missing_flag(self, args, message):
- "Ensure that a particular error raises a missing cli flag error containing message"
- exc = None
- try:
- with mock.patch('certbot.main.sys.stderr'):
- main.main(self.standard_args + args[:]) # NOTE: parser can alter its args!
- except errors.MissingCommandlineFlag as exc_:
- exc = exc_
- self.assertTrue(message in str(exc))
- self.assertTrue(exc is not None)
-
- def test_noninteractive(self):
- args = ['-n', 'certonly']
- self._cli_missing_flag(args, "specify a plugin")
- args.extend(['--standalone', '-d', 'eg.is'])
- self._cli_missing_flag(args, "register before running")
- with mock.patch('certbot.main._auth_from_domains'):
- with mock.patch('certbot.main.client.acme_from_config_key'):
- args.extend(['--email', 'io@io.is'])
- self._cli_missing_flag(args, "--agree-tos")
-
- @mock.patch('certbot.main.renew')
- def test_gui(self, renew):
- args = ['renew', '--dialog']
- # --text conflicts with --dialog
- self.standard_args.remove('--text')
- self._call(args)
- self.assertFalse(renew.call_args[0][0].noninteractive_mode)
-
- @mock.patch('certbot.main.client.acme_client.Client')
- @mock.patch('certbot.main._determine_account')
- @mock.patch('certbot.main.client.Client.obtain_and_enroll_certificate')
- @mock.patch('certbot.main._auth_from_domains')
- def test_user_agent(self, afd, _obt, det, _client):
- # Normally the client is totally mocked out, but here we need more
- # arguments to automate it...
- args = ["--standalone", "certonly", "-m", "none@none.com",
- "-d", "example.com", '--agree-tos'] + self.standard_args
- det.return_value = mock.MagicMock(), None
- afd.return_value = "newcert", mock.MagicMock()
-
- with mock.patch('certbot.main.client.acme_client.ClientNetwork') as acme_net:
- self._call_no_clientmock(args)
- os_ver = util.get_os_info_ua()
- ua = acme_net.call_args[1]["user_agent"]
- self.assertTrue(os_ver in ua)
- import platform
- plat = platform.platform()
- if "linux" in plat.lower():
- self.assertTrue(util.get_os_info_ua() in ua)
-
- with mock.patch('certbot.main.client.acme_client.ClientNetwork') as acme_net:
- ua = "bandersnatch"
- args += ["--user-agent", ua]
- self._call_no_clientmock(args)
- acme_net.assert_called_once_with(mock.ANY, verify_ssl=True, user_agent=ua)
-
- def test_install_abspath(self):
- cert = 'cert'
- key = 'key'
- chain = 'chain'
- fullchain = 'fullchain'
-
- with mock.patch('certbot.main.install') as mock_install:
- self._call(['install', '--cert-path', cert, '--key-path', 'key',
- '--chain-path', 'chain',
- '--fullchain-path', 'fullchain'])
-
- args = mock_install.call_args[0][0]
- self.assertEqual(args.cert_path, os.path.abspath(cert))
- self.assertEqual(args.key_path, os.path.abspath(key))
- self.assertEqual(args.chain_path, os.path.abspath(chain))
- self.assertEqual(args.fullchain_path, os.path.abspath(fullchain))
-
- @mock.patch('certbot.main.plug_sel.record_chosen_plugins')
- @mock.patch('certbot.main.plug_sel.pick_installer')
- def test_installer_selection(self, mock_pick_installer, _rec):
- self._call(['install', '--domains', 'foo.bar', '--cert-path', 'cert',
- '--key-path', 'key', '--chain-path', 'chain'])
- self.assertEqual(mock_pick_installer.call_count, 1)
-
- @mock.patch('certbot.util.exe_exists')
- def test_configurator_selection(self, mock_exe_exists):
- mock_exe_exists.return_value = True
- real_plugins = disco.PluginsRegistry.find_all()
- args = ['--apache', '--authenticator', 'standalone']
-
- # This needed two calls to find_all(), which we're avoiding for now
- # because of possible side effects:
- # https://github.com/letsencrypt/letsencrypt/commit/51ed2b681f87b1eb29088dd48718a54f401e4855
- #with mock.patch('certbot.cli.plugins_testable') as plugins:
- # plugins.return_value = {"apache": True, "nginx": True}
- # ret, _, _, _ = self._call(args)
- # self.assertTrue("Too many flags setting" in ret)
-
- args = ["install", "--nginx", "--cert-path", "/tmp/blah", "--key-path", "/tmp/blah",
- "--nginx-server-root", "/nonexistent/thing", "-d",
- "example.com", "--debug"]
- if "nginx" in real_plugins:
- # Sending nginx a non-existent conf dir will simulate misconfiguration
- # (we can only do that if certbot-nginx is actually present)
- ret, _, _, _ = self._call(args)
- self.assertTrue("The nginx plugin is not working" in ret)
- self.assertTrue("MisconfigurationError" in ret)
-
- self._cli_missing_flag(["--standalone"], "With the standalone plugin, you probably")
-
- with mock.patch("certbot.main._init_le_client") as mock_init:
- with mock.patch("certbot.main._auth_from_domains") as mock_afd:
- mock_afd.return_value = (mock.MagicMock(), mock.MagicMock())
- self._call(["certonly", "--manual", "-d", "foo.bar"])
- unused_config, auth, unused_installer = mock_init.call_args[0]
- self.assertTrue(isinstance(auth, manual.Authenticator))
-
- with mock.patch('certbot.main.obtain_cert') as mock_certonly:
- self._call(["auth", "--standalone"])
- self.assertEqual(1, mock_certonly.call_count)
-
- def test_rollback(self):
- _, _, _, client = self._call(['rollback'])
- self.assertEqual(1, client.rollback.call_count)
-
- _, _, _, client = self._call(['rollback', '--checkpoints', '123'])
- client.rollback.assert_called_once_with(
- mock.ANY, 123, mock.ANY, mock.ANY)
-
- def test_config_changes(self):
- _, _, _, client = self._call(['config_changes'])
- self.assertEqual(1, client.view_config_changes.call_count)
-
- def test_plugins(self):
- flags = ['--init', '--prepare', '--authenticators', '--installers']
- for args in itertools.chain(
- *(itertools.combinations(flags, r)
- for r in six.moves.range(len(flags)))):
- self._call(['plugins'] + list(args))
-
- @mock.patch('certbot.main.plugins_disco')
- @mock.patch('certbot.main.cli.HelpfulArgumentParser.determine_help_topics')
- def test_plugins_no_args(self, _det, mock_disco):
- ifaces = []
- plugins = mock_disco.PluginsRegistry.find_all()
-
- _, stdout, _, _ = self._call(['plugins'])
- plugins.visible.assert_called_once_with()
- plugins.visible().ifaces.assert_called_once_with(ifaces)
- filtered = plugins.visible().ifaces()
- self.assertEqual(stdout.getvalue().strip(), str(filtered))
-
- @mock.patch('certbot.main.plugins_disco')
- @mock.patch('certbot.main.cli.HelpfulArgumentParser.determine_help_topics')
- def test_plugins_init(self, _det, mock_disco):
- ifaces = []
- plugins = mock_disco.PluginsRegistry.find_all()
-
- _, stdout, _, _ = self._call(['plugins', '--init'])
- plugins.visible.assert_called_once_with()
- plugins.visible().ifaces.assert_called_once_with(ifaces)
- filtered = plugins.visible().ifaces()
- self.assertEqual(filtered.init.call_count, 1)
- filtered.verify.assert_called_once_with(ifaces)
- verified = filtered.verify()
- self.assertEqual(stdout.getvalue().strip(), str(verified))
-
- @mock.patch('certbot.main.plugins_disco')
- @mock.patch('certbot.main.cli.HelpfulArgumentParser.determine_help_topics')
- def test_plugins_prepare(self, _det, mock_disco):
- ifaces = []
- plugins = mock_disco.PluginsRegistry.find_all()
- _, stdout, _, _ = self._call(['plugins', '--init', '--prepare'])
- plugins.visible.assert_called_once_with()
- plugins.visible().ifaces.assert_called_once_with(ifaces)
- filtered = plugins.visible().ifaces()
- self.assertEqual(filtered.init.call_count, 1)
- filtered.verify.assert_called_once_with(ifaces)
- verified = filtered.verify()
- verified.prepare.assert_called_once_with()
- verified.available.assert_called_once_with()
- available = verified.available()
- self.assertEqual(stdout.getvalue().strip(), str(available))
-
- def test_certonly_abspath(self):
- cert = 'cert'
- key = 'key'
- chain = 'chain'
- fullchain = 'fullchain'
-
- with mock.patch('certbot.main.obtain_cert') as mock_obtaincert:
- self._call(['certonly', '--cert-path', cert, '--key-path', 'key',
- '--chain-path', 'chain',
- '--fullchain-path', 'fullchain'])
-
- config, unused_plugins = mock_obtaincert.call_args[0]
- self.assertEqual(config.cert_path, os.path.abspath(cert))
- self.assertEqual(config.key_path, os.path.abspath(key))
- self.assertEqual(config.chain_path, os.path.abspath(chain))
- self.assertEqual(config.fullchain_path, os.path.abspath(fullchain))
-
- def test_certonly_bad_args(self):
- try:
- self._call(['-a', 'bad_auth', 'certonly'])
- assert False, "Exception should have been raised"
- except errors.PluginSelectionError as e:
- self.assertTrue('The requested bad_auth plugin does not appear' in str(e))
-
- def test_punycode_ok(self):
- # Punycode is now legal, so no longer an error; instead check
- # that it's _not_ an error (at the initial sanity check stage)
- util.enforce_domain_sanity('this.is.xn--ls8h.tld')
-
- def test_check_config_sanity_domain(self):
- # FQDN
- self.assertRaises(errors.ConfigurationError,
- self._call,
- ['-d', 'a' * 64])
- # FQDN 2
- self.assertRaises(errors.ConfigurationError,
- self._call,
- ['-d', (('a' * 50) + '.') * 10])
- # Wildcard
- self.assertRaises(errors.ConfigurationError,
- self._call,
- ['-d', '*.wildcard.tld'])
-
- # Bare IP address (this is actually a different error message now)
- self.assertRaises(errors.ConfigurationError,
- self._call,
- ['-d', '204.11.231.35'])
-
- def test_csr_with_besteffort(self):
- self.assertRaises(
- errors.Error, self._call,
- 'certonly --csr {0} --allow-subset-of-names'.format(CSR).split())
-
- def test_run_with_csr(self):
- # This is an error because you can only use --csr with certonly
- try:
- self._call(['--csr', CSR])
- except errors.Error as e:
- assert "Please try the certonly" in repr(e)
- return
- assert False, "Expected supplying --csr to fail with default verb"
-
- def test_csr_with_no_domains(self):
- self.assertRaises(
- errors.Error, self._call,
- 'certonly --csr {0}'.format(
- test_util.vector_path('csr-nonames.pem')).split())
-
- def test_csr_with_inconsistent_domains(self):
- self.assertRaises(
- errors.Error, self._call,
- 'certonly -d example.org --csr {0}'.format(CSR).split())
-
- def _get_argument_parser(self):
- plugins = disco.PluginsRegistry.find_all()
- return functools.partial(cli.prepare_and_parse_args, plugins)
+ self.assertTrue(cli.SHORT_USAGE in out)
+ self.assertTrue(cli.COMMAND_OVERVIEW[:100] in out)
+ self.assertTrue("%s" not in out)
+ self.assertTrue("{0}" not in out)
def test_parse_domains(self):
- parse = self._get_argument_parser()
-
short_args = ['-d', 'example.com']
- namespace = parse(short_args)
+ namespace = self.parse(short_args)
self.assertEqual(namespace.domains, ['example.com'])
short_args = ['-d', 'trailing.period.com.']
- namespace = parse(short_args)
+ namespace = self.parse(short_args)
self.assertEqual(namespace.domains, ['trailing.period.com'])
short_args = ['-d', 'example.com,another.net,third.org,example.com']
- namespace = parse(short_args)
+ namespace = self.parse(short_args)
self.assertEqual(namespace.domains, ['example.com', 'another.net',
'third.org'])
long_args = ['--domains', 'example.com']
- namespace = parse(long_args)
+ namespace = self.parse(long_args)
self.assertEqual(namespace.domains, ['example.com'])
long_args = ['--domains', 'trailing.period.com.']
- namespace = parse(long_args)
+ namespace = self.parse(long_args)
self.assertEqual(namespace.domains, ['trailing.period.com'])
long_args = ['--domains', 'example.com,another.net,example.com']
- namespace = parse(long_args)
+ namespace = self.parse(long_args)
self.assertEqual(namespace.domains, ['example.com', 'another.net'])
def test_preferred_challenges(self):
from acme.challenges import HTTP01, TLSSNI01, DNS01
- parse = self._get_argument_parser()
short_args = ['--preferred-challenges', 'http, tls-sni-01, dns']
- namespace = parse(short_args)
+ namespace = self.parse(short_args)
self.assertEqual(namespace.pref_challs, [HTTP01, TLSSNI01, DNS01])
short_args = ['--preferred-challenges', 'jumping-over-the-moon']
- self.assertRaises(argparse.ArgumentTypeError, parse, short_args)
+ self.assertRaises(argparse.ArgumentTypeError, self.parse, short_args)
def test_server_flag(self):
- parse = self._get_argument_parser()
- namespace = parse('--server example.com'.split())
+ namespace = self.parse('--server example.com'.split())
self.assertEqual(namespace.server, 'example.com')
+ def test_must_staple_flag(self):
+ short_args = ['--must-staple']
+ namespace = self.parse(short_args)
+ self.assertTrue(namespace.must_staple)
+ self.assertTrue(namespace.staple)
+
+ def test_no_gui(self):
+ args = ['renew', '--dialog']
+ stderr = six.StringIO()
+ with mock.patch('certbot.main.sys.stderr', new=stderr):
+ namespace = self.parse(args)
+
+ self.assertTrue(namespace.noninteractive_mode)
+ self.assertTrue("--dialog is deprecated" in stderr.getvalue())
+
def _check_server_conflict_message(self, parser_args, conflicting_args):
- parse = self._get_argument_parser()
try:
- parse(parser_args)
+ self.parse(parser_args)
self.fail( # pragma: no cover
"The following flags didn't conflict with "
'--server: {0}'.format(', '.join(conflicting_args)))
@@ -456,36 +200,15 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods
for arg in conflicting_args:
self.assertTrue(arg in str(error))
- def test_must_staple_flag(self):
- parse = self._get_argument_parser()
- short_args = ['--must-staple']
- namespace = parse(short_args)
- self.assertTrue(namespace.must_staple)
- self.assertTrue(namespace.staple)
-
def test_staging_flag(self):
- parse = self._get_argument_parser()
short_args = ['--staging']
- namespace = parse(short_args)
+ namespace = self.parse(short_args)
self.assertTrue(namespace.staging)
self.assertEqual(namespace.server, constants.STAGING_URI)
short_args += '--server example.com'.split()
self._check_server_conflict_message(short_args, '--staging')
- def test_option_was_set(self):
- key_size_option = 'rsa_key_size'
- key_size_value = cli.flag_default(key_size_option)
- self._get_argument_parser()(
- '--rsa-key-size {0}'.format(key_size_value).split())
-
- self.assertTrue(cli.option_was_set(key_size_option, key_size_value))
- self.assertTrue(cli.option_was_set('no_verify_ssl', True))
-
- config_dir_option = 'config_dir'
- self.assertFalse(cli.option_was_set(
- config_dir_option, cli.flag_default(config_dir_option)))
-
def _assert_dry_run_flag_worked(self, namespace, existing_account):
self.assertTrue(namespace.dry_run)
self.assertTrue(namespace.break_my_certs)
@@ -500,26 +223,25 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods
self.assertFalse(namespace.register_unsafely_without_email)
def test_dry_run_flag(self):
- parse = self._get_argument_parser()
config_dir = tempfile.mkdtemp()
short_args = '--dry-run --config-dir {0}'.format(config_dir).split()
- self.assertRaises(errors.Error, parse, short_args)
+ self.assertRaises(errors.Error, self.parse, short_args)
self._assert_dry_run_flag_worked(
- parse(short_args + ['auth']), False)
+ self.parse(short_args + ['auth']), False)
self._assert_dry_run_flag_worked(
- parse(short_args + ['certonly']), False)
+ self.parse(short_args + ['certonly']), False)
self._assert_dry_run_flag_worked(
- parse(short_args + ['renew']), False)
+ self.parse(short_args + ['renew']), False)
account_dir = os.path.join(config_dir, constants.ACCOUNTS_DIR)
os.mkdir(account_dir)
os.mkdir(os.path.join(account_dir, 'fake_account_dir'))
- self._assert_dry_run_flag_worked(parse(short_args + ['auth']), True)
- self._assert_dry_run_flag_worked(parse(short_args + ['renew']), True)
+ self._assert_dry_run_flag_worked(self.parse(short_args + ['auth']), True)
+ self._assert_dry_run_flag_worked(self.parse(short_args + ['renew']), True)
short_args += ['certonly']
- self._assert_dry_run_flag_worked(parse(short_args), True)
+ self._assert_dry_run_flag_worked(self.parse(short_args), True)
short_args += '--server example.com'.split()
conflicts = ['--dry-run']
@@ -529,666 +251,38 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods
conflicts += ['--staging']
self._check_server_conflict_message(short_args, conflicts)
- def _certonly_new_request_common(self, mock_client, args=None):
- with mock.patch('certbot.main._treat_as_renewal') as mock_renewal:
- mock_renewal.return_value = ("newcert", None)
- with mock.patch('certbot.main._init_le_client') as mock_init:
- mock_init.return_value = mock_client
- if args is None:
- args = []
- args += '-d foo.bar -a standalone certonly'.split()
- self._call(args)
+ def test_option_was_set(self):
+ key_size_option = 'rsa_key_size'
+ key_size_value = cli.flag_default(key_size_option)
+ self.parse('--rsa-key-size {0}'.format(key_size_value).split())
- @mock.patch('certbot.main.zope.component.getUtility')
- def test_certonly_dry_run_new_request_success(self, mock_get_utility):
- mock_client = mock.MagicMock()
- mock_client.obtain_and_enroll_certificate.return_value = None
- self._certonly_new_request_common(mock_client, ['--dry-run'])
- self.assertEqual(
- mock_client.obtain_and_enroll_certificate.call_count, 1)
- self.assertTrue(
- 'dry run' in mock_get_utility().add_message.call_args[0][0])
- # Asserts we don't suggest donating after a successful dry run
- self.assertEqual(mock_get_utility().add_message.call_count, 1)
+ self.assertTrue(cli.option_was_set(key_size_option, key_size_value))
+ self.assertTrue(cli.option_was_set('no_verify_ssl', True))
- @mock.patch('certbot.crypto_util.notAfter')
- @mock.patch('certbot.main.zope.component.getUtility')
- def test_certonly_new_request_success(self, mock_get_utility, mock_notAfter):
- cert_path = '/etc/letsencrypt/live/foo.bar'
- date = '1970-01-01'
- mock_notAfter().date.return_value = date
+ config_dir_option = 'config_dir'
+ self.assertFalse(cli.option_was_set(
+ config_dir_option, cli.flag_default(config_dir_option)))
- mock_lineage = mock.MagicMock(cert=cert_path, fullchain=cert_path)
- mock_client = mock.MagicMock()
- mock_client.obtain_and_enroll_certificate.return_value = mock_lineage
- self._certonly_new_request_common(mock_client)
- self.assertEqual(
- mock_client.obtain_and_enroll_certificate.call_count, 1)
- 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(
- 'donate' in mock_get_utility().add_message.call_args[0][0])
+ def test_encode_revocation_reason(self):
+ for reason, code in constants.REVOCATION_REASONS.items():
+ namespace = self.parse(['--reason', reason])
+ self.assertEqual(namespace.reason, code)
+ for reason, code in constants.REVOCATION_REASONS.items():
+ namespace = self.parse(['--reason', reason.upper()])
+ self.assertEqual(namespace.reason, code)
- def test_certonly_new_request_failure(self):
- mock_client = mock.MagicMock()
- mock_client.obtain_and_enroll_certificate.return_value = False
- self.assertRaises(errors.Error,
- self._certonly_new_request_common, mock_client)
-
- def _test_renewal_common(self, due_for_renewal, extra_args, log_out=None,
- args=None, should_renew=True, error_expected=False):
- # pylint: disable=too-many-locals,too-many-arguments
- cert_path = test_util.vector_path('cert.pem')
- chain_path = '/etc/letsencrypt/live/foo.bar/fullchain.pem'
- mock_lineage = mock.MagicMock(cert=cert_path, fullchain=chain_path)
- mock_lineage.should_autorenew.return_value = due_for_renewal
- mock_lineage.has_pending_deployment.return_value = False
- mock_certr = mock.MagicMock()
- mock_key = mock.MagicMock(pem='pem_key')
- mock_client = mock.MagicMock()
- stdout = None
- mock_client.obtain_certificate.return_value = (mock_certr, 'chain',
- mock_key, 'csr')
- try:
- with mock.patch('certbot.main._find_duplicative_certs') as mock_fdc:
- mock_fdc.return_value = (mock_lineage, None)
- with mock.patch('certbot.main._init_le_client') as mock_init:
- mock_init.return_value = mock_client
- get_utility_path = 'certbot.main.zope.component.getUtility'
- with mock.patch(get_utility_path) as mock_get_utility:
- with mock.patch('certbot.main.renewal.OpenSSL') as mock_ssl:
- mock_latest = mock.MagicMock()
- mock_latest.get_issuer.return_value = "Fake fake"
- mock_ssl.crypto.load_certificate.return_value = mock_latest
- with mock.patch('certbot.main.renewal.crypto_util'):
- if not args:
- args = ['-d', 'isnot.org', '-a', 'standalone', 'certonly']
- if extra_args:
- args += extra_args
- try:
- ret, stdout, _, _ = self._call(args)
- if ret:
- print("Returned", ret)
- raise AssertionError(ret)
- assert not error_expected, "renewal should have errored"
- except: # pylint: disable=bare-except
- if not error_expected:
- raise AssertionError(
- "Unexpected renewal error:\n" +
- traceback.format_exc())
-
- if should_renew:
- mock_client.obtain_certificate.assert_called_once_with(['isnot.org'])
- else:
- self.assertEqual(mock_client.obtain_certificate.call_count, 0)
- except:
- self._dump_log()
- raise
- finally:
- if log_out:
- with open(os.path.join(self.logs_dir, "letsencrypt.log")) as lf:
- self.assertTrue(log_out in lf.read())
-
- return mock_lineage, mock_get_utility, stdout
-
- def test_certonly_renewal(self):
- lineage, get_utility, _ = self._test_renewal_common(True, [])
- self.assertEqual(lineage.save_successor.call_count, 1)
- lineage.update_all_links_to.assert_called_once_with(
- lineage.latest_common_version())
- cert_msg = get_utility().add_message.call_args_list[0][0][0]
- self.assertTrue('fullchain.pem' in cert_msg)
- self.assertTrue('donate' in get_utility().add_message.call_args[0][0])
-
- def test_certonly_renewal_triggers(self):
- # --dry-run should force renewal
- _, get_utility, _ = self._test_renewal_common(False, ['--dry-run', '--keep'],
- log_out="simulating renewal")
- self.assertEqual(get_utility().add_message.call_count, 1)
- self.assertTrue('dry run' in get_utility().add_message.call_args[0][0])
-
- self._test_renewal_common(False, ['--renew-by-default', '-tvv', '--debug'],
- log_out="Auto-renewal forced")
- self.assertEqual(get_utility().add_message.call_count, 1)
-
- self._test_renewal_common(False, ['-tvv', '--debug', '--keep'],
- log_out="not yet due", should_renew=False)
-
- def _dump_log(self):
- with open(os.path.join(self.logs_dir, "letsencrypt.log")) as lf:
- print("Logs:")
- print(lf.read())
-
- def _make_lineage(self, testfile):
- """Creates a lineage defined by testfile.
-
- This creates the archive, live, and renewal directories if
- necessary and creates a simple lineage.
-
- :param str testfile: configuration file to base the lineage on
-
- :returns: path to the renewal conf file for the created lineage
- :rtype: str
-
- """
- lineage_name = testfile[:-len('.conf')]
-
- conf_dir = os.path.join(
- self.config_dir, constants.RENEWAL_CONFIGS_DIR)
- archive_dir = os.path.join(
- self.config_dir, constants.ARCHIVE_DIR, lineage_name)
- live_dir = os.path.join(
- self.config_dir, constants.LIVE_DIR, lineage_name)
-
- for directory in (archive_dir, conf_dir, live_dir,):
- if not os.path.exists(directory):
- os.makedirs(directory)
-
- sample_archive = test_util.vector_path('sample-archive')
- for kind in os.listdir(sample_archive):
- shutil.copyfile(os.path.join(sample_archive, kind),
- os.path.join(archive_dir, kind))
-
- for kind in storage.ALL_FOUR:
- os.symlink(os.path.join(archive_dir, '{0}1.pem'.format(kind)),
- os.path.join(live_dir, '{0}.pem'.format(kind)))
-
- conf_path = os.path.join(self.config_dir, conf_dir, testfile)
- with open(test_util.vector_path(testfile)) as src:
- with open(conf_path, 'w') as dst:
- dst.writelines(
- line.replace('MAGICDIR', self.config_dir) for line in src)
-
- return conf_path
-
- def test_renew_verb(self):
- self._make_lineage('sample-renewal.conf')
- args = ["renew", "--dry-run", "-tvv"]
- self._test_renewal_common(True, [], args=args, should_renew=True)
-
- def test_quiet_renew(self):
- self._make_lineage('sample-renewal.conf')
- args = ["renew", "--dry-run"]
- _, _, stdout = self._test_renewal_common(True, [], args=args, should_renew=True)
- out = stdout.getvalue()
- self.assertTrue("renew" in out)
-
- args = ["renew", "--dry-run", "-q"]
- _, _, stdout = self._test_renewal_common(True, [], args=args, should_renew=True)
- out = stdout.getvalue()
- self.assertEqual("", out)
-
- def test_renew_hook_validation(self):
- self._make_lineage('sample-renewal.conf')
- args = ["renew", "--dry-run", "--post-hook=no-such-command"]
- self._test_renewal_common(True, [], args=args, should_renew=False,
- error_expected=True)
-
- def test_renew_no_hook_validation(self):
- self._make_lineage('sample-renewal.conf')
- args = ["renew", "--dry-run", "--post-hook=no-such-command",
- "--disable-hook-validation"]
- self._test_renewal_common(True, [], args=args, should_renew=True,
- error_expected=False)
-
- @mock.patch("certbot.cli.set_by_cli")
- def test_ancient_webroot_renewal_conf(self, mock_set_by_cli):
- mock_set_by_cli.return_value = False
- rc_path = self._make_lineage('sample-renewal-ancient.conf')
- args = mock.MagicMock(account=None, email=None, webroot_path=None)
- config = configuration.NamespaceConfig(args)
- lineage = storage.RenewableCert(rc_path,
- configuration.RenewerConfiguration(config))
- renewalparams = lineage.configuration["renewalparams"]
- # pylint: disable=protected-access
- renewal._restore_webroot_config(config, renewalparams)
- self.assertEqual(config.webroot_path, ["/var/www/"])
-
- def test_renew_verb_empty_config(self):
- rd = os.path.join(self.config_dir, 'renewal')
- if not os.path.exists(rd):
- os.makedirs(rd)
- with open(os.path.join(rd, 'empty.conf'), 'w'):
- pass # leave the file empty
- args = ["renew", "--dry-run", "-tvv"]
- self._test_renewal_common(False, [], args=args, should_renew=False, error_expected=True)
-
- def _make_dummy_renewal_config(self):
- renewer_configs_dir = os.path.join(self.config_dir, 'renewal')
- os.makedirs(renewer_configs_dir)
- with open(os.path.join(renewer_configs_dir, 'test.conf'), 'w') as f:
- f.write("My contents don't matter")
-
- def _test_renew_common(self, renewalparams=None, names=None,
- assert_oc_called=None, **kwargs):
- self._make_dummy_renewal_config()
- with mock.patch('certbot.storage.RenewableCert') as mock_rc:
- mock_lineage = mock.MagicMock()
- mock_lineage.fullchain = "somepath/fullchain.pem"
- if renewalparams is not None:
- mock_lineage.configuration = {'renewalparams': renewalparams}
- if names is not None:
- mock_lineage.names.return_value = names
- mock_rc.return_value = mock_lineage
- with mock.patch('certbot.main.obtain_cert') as mock_obtain_cert:
- kwargs.setdefault('args', ['renew'])
- self._test_renewal_common(True, None, should_renew=False, **kwargs)
-
- if assert_oc_called is not None:
- if assert_oc_called:
- self.assertTrue(mock_obtain_cert.called)
- else:
- self.assertFalse(mock_obtain_cert.called)
-
- def test_renew_no_renewalparams(self):
- self._test_renew_common(assert_oc_called=False, error_expected=True)
-
- def test_renew_no_authenticator(self):
- self._test_renew_common(renewalparams={}, assert_oc_called=False,
- error_expected=True)
-
- def test_renew_with_bad_int(self):
- renewalparams = {'authenticator': 'webroot',
- 'rsa_key_size': 'over 9000'}
- self._test_renew_common(renewalparams=renewalparams, error_expected=True,
- assert_oc_called=False)
-
- def test_renew_with_nonetype_http01(self):
- renewalparams = {'authenticator': 'webroot',
- 'http01_port': 'None'}
- self._test_renew_common(renewalparams=renewalparams,
- assert_oc_called=True)
-
- def test_renew_with_bad_domain(self):
- renewalparams = {'authenticator': 'webroot'}
- names = ['*.example.com']
- self._test_renew_common(renewalparams=renewalparams, error_expected=True,
- names=names, assert_oc_called=False)
-
- def test_renew_with_configurator(self):
- renewalparams = {'authenticator': 'webroot'}
- self._test_renew_common(
- renewalparams=renewalparams, assert_oc_called=True,
- args='renew --configurator apache'.split())
-
- def test_renew_plugin_config_restoration(self):
- renewalparams = {'authenticator': 'webroot',
- 'webroot_path': 'None',
- 'webroot_imaginary_flag': '42'}
- self._test_renew_common(renewalparams=renewalparams,
- assert_oc_called=True)
-
- def test_renew_with_webroot_map(self):
- renewalparams = {'authenticator': 'webroot'}
- self._test_renew_common(
- renewalparams=renewalparams, assert_oc_called=True,
- args=['renew', '--webroot-map', '{"example.com": "/tmp"}'])
-
- def test_renew_reconstitute_error(self):
- # pylint: disable=protected-access
- with mock.patch('certbot.main.renewal._reconstitute') as mock_reconstitute:
- mock_reconstitute.side_effect = Exception
- self._test_renew_common(assert_oc_called=False, error_expected=True)
-
- def test_renew_obtain_cert_error(self):
- self._make_dummy_renewal_config()
- with mock.patch('certbot.storage.RenewableCert') as mock_rc:
- mock_lineage = mock.MagicMock()
- mock_lineage.fullchain = "somewhere/fullchain.pem"
- mock_rc.return_value = mock_lineage
- mock_lineage.configuration = {
- 'renewalparams': {'authenticator': 'webroot'}}
- with mock.patch('certbot.main.obtain_cert') as mock_obtain_cert:
- mock_obtain_cert.side_effect = Exception
- self._test_renewal_common(True, None, error_expected=True,
- args=['renew'], should_renew=False)
-
- def test_renew_with_bad_cli_args(self):
- self._test_renewal_common(True, None, args='renew -d example.com'.split(),
- should_renew=False, error_expected=True)
- self._test_renewal_common(True, None, args='renew --csr {0}'.format(CSR).split(),
- should_renew=False, error_expected=True)
-
- @mock.patch('certbot.main.zope.component.getUtility')
- @mock.patch('certbot.main._treat_as_renewal')
- @mock.patch('certbot.main._init_le_client')
- def test_certonly_reinstall(self, mock_init, mock_renewal, mock_get_utility):
- mock_renewal.return_value = ('reinstall', mock.MagicMock())
- mock_init.return_value = mock_client = mock.MagicMock()
- self._call(['-d', 'foo.bar', '-a', 'standalone', 'certonly'])
- self.assertFalse(mock_client.obtain_certificate.called)
- self.assertFalse(mock_client.obtain_and_enroll_certificate.called)
- self.assertEqual(mock_get_utility().add_message.call_count, 0)
- #self.assertTrue('donate' not in mock_get_utility().add_message.call_args[0][0])
-
- def _test_certonly_csr_common(self, extra_args=None):
- certr = 'certr'
- chain = 'chain'
- mock_client = mock.MagicMock()
- mock_client.obtain_certificate_from_csr.return_value = (certr, chain)
- cert_path = '/etc/letsencrypt/live/example.com/cert.pem'
- mock_client.save_certificate.return_value = cert_path, None, None
- with mock.patch('certbot.main._init_le_client') as mock_init:
- mock_init.return_value = mock_client
- get_utility_path = 'certbot.main.zope.component.getUtility'
- with mock.patch(get_utility_path) as mock_get_utility:
- chain_path = '/etc/letsencrypt/live/example.com/chain.pem'
- full_path = '/etc/letsencrypt/live/example.com/fullchain.pem'
- args = ('-a standalone certonly --csr {0} --cert-path {1} '
- '--chain-path {2} --fullchain-path {3}').format(
- CSR, cert_path, chain_path, full_path).split()
- if extra_args:
- args += extra_args
- with mock.patch('certbot.main.crypto_util'):
- self._call(args)
-
- if '--dry-run' in args:
- self.assertFalse(mock_client.save_certificate.called)
- else:
- mock_client.save_certificate.assert_called_once_with(
- certr, chain, cert_path, chain_path, full_path)
-
- return mock_get_utility
-
- def test_certonly_csr(self):
- mock_get_utility = self._test_certonly_csr_common()
- cert_msg = mock_get_utility().add_message.call_args_list[0][0][0]
- self.assertTrue('cert.pem' in cert_msg)
- self.assertTrue(
- 'donate' in mock_get_utility().add_message.call_args[0][0])
-
- def test_certonly_csr_dry_run(self):
- mock_get_utility = self._test_certonly_csr_common(['--dry-run'])
- self.assertEqual(mock_get_utility().add_message.call_count, 1)
- self.assertTrue(
- 'dry run' in mock_get_utility().add_message.call_args[0][0])
-
- @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,
- '--server', server, 'revoke'])
- with open(KEY, '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:
- 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.patch('certbot.main._determine_account')
- def test_revoke_without_key(self, mock_determine_account):
- mock_determine_account.return_value = (mock.MagicMock(), None)
- _, _, _, client = self._call(['--cert-path', CERT, 'revoke'])
- with open(CERT) as f:
- cert = crypto_util.pyopenssl_load_certificate(f.read())[0]
- mock_revoke = client.acme_from_config_key().revoke
- mock_revoke.assert_called_once_with(jose.ComparableX509(cert))
-
- @mock.patch('certbot.main.sys')
- def test_handle_exception(self, mock_sys):
- # pylint: disable=protected-access
- from acme import messages
-
- config = mock.MagicMock()
- mock_open = mock.mock_open()
-
- with mock.patch('certbot.main.open', mock_open, create=True):
- exception = Exception('detail')
- config.verbose_count = 1
- main._handle_exception(
- Exception, exc_value=exception, trace=None, config=None)
- mock_open().write.assert_any_call(''.join(
- traceback.format_exception_only(Exception, exception)))
- error_msg = mock_sys.exit.call_args_list[0][0][0]
- self.assertTrue('unexpected error' in error_msg)
-
- with mock.patch('certbot.main.open', mock_open, create=True):
- mock_open.side_effect = [KeyboardInterrupt]
- error = errors.Error('detail')
- main._handle_exception(
- errors.Error, exc_value=error, trace=None, config=None)
- # assert_any_call used because sys.exit doesn't exit in cli.py
- mock_sys.exit.assert_any_call(''.join(
- traceback.format_exception_only(errors.Error, error)))
-
- bad_typ = messages.ERROR_PREFIX + 'triffid'
- exception = messages.Error(detail='alpha', typ=bad_typ, title='beta')
- config = mock.MagicMock(debug=False, verbose_count=-3)
- main._handle_exception(
- messages.Error, exc_value=exception, trace=None, config=config)
- error_msg = mock_sys.exit.call_args_list[-1][0][0]
- self.assertTrue('unexpected error' in error_msg)
- self.assertTrue('acme:error' not in error_msg)
- self.assertTrue('alpha' in error_msg)
- self.assertTrue('beta' in error_msg)
- config = mock.MagicMock(debug=False, verbose_count=1)
- main._handle_exception(
- messages.Error, exc_value=exception, trace=None, config=config)
- error_msg = mock_sys.exit.call_args_list[-1][0][0]
- self.assertTrue('unexpected error' in error_msg)
- self.assertTrue('acme:error' in error_msg)
- self.assertTrue('alpha' in error_msg)
-
- interrupt = KeyboardInterrupt('detail')
- main._handle_exception(
- KeyboardInterrupt, exc_value=interrupt, trace=None, config=None)
- mock_sys.exit.assert_called_with(''.join(
- traceback.format_exception_only(KeyboardInterrupt, interrupt)))
-
- # Test dialog errors
- exception = dialog.error(message="test message")
- main._handle_exception(
- dialog.DialogError, exc_value=exception, trace=None, config=None)
- error_msg = mock_sys.exit.call_args_list[-1][0][0]
- self.assertTrue("test message" in error_msg)
-
- def test_read_file(self):
- rel_test_path = os.path.relpath(os.path.join(self.tmp_dir, 'foo'))
+ def test_force_interactive(self):
self.assertRaises(
- argparse.ArgumentTypeError, cli.read_file, rel_test_path)
-
- test_contents = b'bar\n'
- with open(rel_test_path, 'wb') as f:
- f.write(test_contents)
-
- path, contents = cli.read_file(rel_test_path)
- self.assertEqual(path, os.path.abspath(path))
- self.assertEqual(contents, test_contents)
-
- def test_agree_dev_preview_config(self):
- with mock.patch('certbot.main.run') as mocked_run:
- self._call(['-c', test_util.vector_path('cli.ini')])
- self.assertTrue(mocked_run.called)
-
- def test_register(self):
- with mock.patch('certbot.main.client') as mocked_client:
- acc = mock.MagicMock()
- acc.id = "imaginary_account"
- mocked_client.register.return_value = (acc, "worked")
- self._call_no_clientmock(["register", "--email", "user@example.org"])
- # TODO: It would be more correct to explicitly check that
- # _determine_account() gets called in the above case,
- # but coverage statistics should also show that it did.
- with mock.patch('certbot.main.account') as mocked_account:
- mocked_storage = mock.MagicMock()
- mocked_account.AccountFileStorage.return_value = mocked_storage
- mocked_storage.find_all.return_value = ["an account"]
- x = self._call_no_clientmock(["register", "--email", "user@example.org"])
- self.assertTrue("There is an existing account" in x[0])
-
- def test_update_registration_no_existing_accounts(self):
- # with mock.patch('certbot.main.client') as mocked_client:
- with mock.patch('certbot.main.account') as mocked_account:
- mocked_storage = mock.MagicMock()
- mocked_account.AccountFileStorage.return_value = mocked_storage
- mocked_storage.find_all.return_value = []
- x = self._call_no_clientmock(
- ["register", "--update-registration", "--email",
- "user@example.org"])
- self.assertTrue("Could not find an existing account" in x[0])
-
- def test_update_registration_unsafely(self):
- # This test will become obsolete when register --update-registration
- # supports removing an e-mail address from the account
- with mock.patch('certbot.main.account') as mocked_account:
- mocked_storage = mock.MagicMock()
- mocked_account.AccountFileStorage.return_value = mocked_storage
- mocked_storage.find_all.return_value = ["an account"]
- x = self._call_no_clientmock(
- "register --update-registration "
- "--register-unsafely-without-email".split())
- self.assertTrue("--register-unsafely-without-email" in x[0])
-
- @mock.patch('certbot.main.display_ops.get_email')
- @mock.patch('certbot.main.zope.component.getUtility')
- def test_update_registration_with_email(self, mock_utility, mock_email):
- email = "user@example.com"
- mock_email.return_value = email
- with mock.patch('certbot.main.client') as mocked_client:
- with mock.patch('certbot.main.account') as mocked_account:
- with mock.patch('certbot.main._determine_account') as mocked_det:
- with mock.patch('certbot.main.client') as mocked_client:
- mocked_storage = mock.MagicMock()
- mocked_account.AccountFileStorage.return_value = mocked_storage
- mocked_storage.find_all.return_value = ["an account"]
- mocked_det.return_value = (mock.MagicMock(), "foo")
- acme_client = mock.MagicMock()
- mocked_client.Client.return_value = acme_client
- x = self._call_no_clientmock(
- ["register", "--update-registration"])
- # When registration change succeeds, the return value
- # of register() is None
- self.assertTrue(x[0] is None)
- # and we got supposedly did update the registration from
- # the server
- self.assertTrue(
- acme_client.acme.update_registration.called)
- # and we saved the updated registration on disk
- self.assertTrue(mocked_storage.save_regr.called)
- self.assertTrue(
- email in mock_utility().add_message.call_args[0][0])
-
- def test_conflicting_args(self):
- args = ['renew', '--dialog', '--text']
- self.assertRaises(errors.Error, self._call, args)
-
- def test_text_mode_when_verbose(self):
- parse = self._get_argument_parser()
- short_args = ['-v']
- namespace = parse(short_args)
- self.assertTrue(namespace.text_mode)
-
-
-class DetermineAccountTest(unittest.TestCase):
- """Tests for certbot.cli._determine_account."""
-
- def setUp(self):
- self.args = mock.MagicMock(account=None, email=None,
- register_unsafely_without_email=False)
- self.config = configuration.NamespaceConfig(self.args)
- self.accs = [mock.MagicMock(id='x'), mock.MagicMock(id='y')]
- self.account_storage = account.AccountMemoryStorage()
-
- def _call(self):
- # pylint: disable=protected-access
- from certbot.main import _determine_account
- with mock.patch('certbot.main.account.AccountFileStorage') as mock_storage:
- mock_storage.return_value = self.account_storage
- return _determine_account(self.config)
-
- def test_args_account_set(self):
- self.account_storage.save(self.accs[1])
- self.config.account = self.accs[1].id
- self.assertEqual((self.accs[1], None), self._call())
- self.assertEqual(self.accs[1].id, self.config.account)
- self.assertTrue(self.config.email is None)
-
- def test_single_account(self):
- self.account_storage.save(self.accs[0])
- self.assertEqual((self.accs[0], None), self._call())
- self.assertEqual(self.accs[0].id, self.config.account)
- self.assertTrue(self.config.email is None)
-
- @mock.patch('certbot.client.display_ops.choose_account')
- def test_multiple_accounts(self, mock_choose_accounts):
- for acc in self.accs:
- self.account_storage.save(acc)
- mock_choose_accounts.return_value = self.accs[1]
- self.assertEqual((self.accs[1], None), self._call())
- self.assertEqual(
- set(mock_choose_accounts.call_args[0][0]), set(self.accs))
- self.assertEqual(self.accs[1].id, self.config.account)
- self.assertTrue(self.config.email is None)
-
- @mock.patch('certbot.client.display_ops.get_email')
- def test_no_accounts_no_email(self, mock_get_email):
- mock_get_email.return_value = 'foo@bar.baz'
-
- with mock.patch('certbot.main.client') as client:
- client.register.return_value = (
- self.accs[0], mock.sentinel.acme)
- self.assertEqual((self.accs[0], mock.sentinel.acme), self._call())
- client.register.assert_called_once_with(
- self.config, self.account_storage, tos_cb=mock.ANY)
-
- self.assertEqual(self.accs[0].id, self.config.account)
- self.assertEqual('foo@bar.baz', self.config.email)
-
- def test_no_accounts_email(self):
- self.config.email = 'other email'
- with mock.patch('certbot.main.client') as client:
- client.register.return_value = (self.accs[1], mock.sentinel.acme)
- self._call()
- self.assertEqual(self.accs[1].id, self.config.account)
- self.assertEqual('other email', self.config.email)
-
-
-class DuplicativeCertsTest(storage_test.BaseRenewableCertTest):
- """Test to avoid duplicate lineages."""
-
- def setUp(self):
- super(DuplicativeCertsTest, self).setUp()
- self.config.write()
- self._write_out_ex_kinds()
-
- def tearDown(self):
- shutil.rmtree(self.tempdir)
-
- @mock.patch('certbot.util.make_or_verify_dir')
- def test_find_duplicative_names(self, unused_makedir):
- from certbot.main import _find_duplicative_certs
- test_cert = test_util.load_vector('cert-san.pem')
- with open(self.test_rc.cert, 'wb') as f:
- f.write(test_cert)
-
- # No overlap at all
- result = _find_duplicative_certs(
- self.cli_config, ['wow.net', 'hooray.org'])
- self.assertEqual(result, (None, None))
-
- # Totally identical
- result = _find_duplicative_certs(
- self.cli_config, ['example.com', 'www.example.com'])
- self.assertTrue(result[0].configfile.filename.endswith('example.org.conf'))
- self.assertEqual(result[1], None)
-
- # Superset
- result = _find_duplicative_certs(
- self.cli_config, ['example.com', 'www.example.com', 'something.new'])
- self.assertEqual(result[0], None)
- self.assertTrue(result[1].configfile.filename.endswith('example.org.conf'))
-
- # Partial overlap doesn't count
- result = _find_duplicative_certs(
- self.cli_config, ['example.com', 'something.new'])
- self.assertEqual(result, (None, None))
+ errors.Error, self.parse, "renew --force-interactive".split())
+ self.assertRaises(
+ errors.Error, self.parse, "-n --force-interactive".split())
class DefaultTest(unittest.TestCase):
"""Tests for certbot.cli._Default."""
+ _multiprocess_can_split_ = True
+
def setUp(self):
# pylint: disable=protected-access
self.default1 = cli._Default()
@@ -1208,6 +302,8 @@ class DefaultTest(unittest.TestCase):
class SetByCliTest(unittest.TestCase):
"""Tests for certbot.set_by_cli and related functions."""
+ _multiprocess_can_split_ = True
+
def setUp(self):
reload_module(cli)
@@ -1218,22 +314,22 @@ class SetByCliTest(unittest.TestCase):
def test_report_config_interaction_str(self):
cli.report_config_interaction('manual_public_ip_logging_ok',
- 'manual_test_mode')
- cli.report_config_interaction('manual_test_mode', 'manual')
+ 'manual_auth_hook')
+ cli.report_config_interaction('manual_auth_hook', 'manual')
self._test_report_config_interaction_common()
def test_report_config_interaction_iterable(self):
cli.report_config_interaction(('manual_public_ip_logging_ok',),
- ('manual_test_mode',))
- cli.report_config_interaction(('manual_test_mode',), ('manual',))
+ ('manual_auth_hook',))
+ cli.report_config_interaction(('manual_auth_hook',), ('manual',))
self._test_report_config_interaction_common()
def _test_report_config_interaction_common(self):
"""Tests implied interaction between manual flags.
- --manual implies --manual-test-mode which implies
+ --manual implies --manual-auth-hook which implies
--manual-public-ip-logging-ok. These interactions don't actually
exist in the client, but are used here for testing purposes.
@@ -1241,13 +337,13 @@ class SetByCliTest(unittest.TestCase):
args = ['--manual']
verb = 'renew'
- for v in ('manual', 'manual_test_mode', 'manual_public_ip_logging_ok'):
+ for v in ('manual', 'manual_auth_hook', 'manual_public_ip_logging_ok'):
self.assertTrue(_call_set_by_cli(v, args, verb))
cli.set_by_cli.detector = None
- args = ['--manual-test-mode']
- for v in ('manual_test_mode', 'manual_public_ip_logging_ok'):
+ args = ['--manual-auth-hook', 'command']
+ for v in ('manual_auth_hook', 'manual_public_ip_logging_ok'):
self.assertTrue(_call_set_by_cli(v, args, verb))
self.assertFalse(_call_set_by_cli('manual', args, verb))
diff --git a/certbot/tests/client_test.py b/certbot/tests/client_test.py
index d61025116..f4b86fc7c 100644
--- a/certbot/tests/client_test.py
+++ b/certbot/tests/client_test.py
@@ -13,7 +13,7 @@ from certbot import account
from certbot import errors
from certbot import util
-from certbot.tests import test_util
+import certbot.tests.util as test_util
KEY = test_util.load_vector("rsa512_key.pem")
@@ -102,15 +102,18 @@ class RegisterTest(unittest.TestCase):
mock_client().register.side_effect = [mx_err, mock.MagicMock()]
self.assertRaises(messages.Error, self._call)
-class ClientTest(unittest.TestCase):
- """Tests for certbot.client.Client."""
+class ClientTestCommon(unittest.TestCase):
+ """Common base class for certbot.client.Client tests."""
def setUp(self):
self.config = mock.MagicMock(
- no_verify_ssl=False, config_dir="/etc/letsencrypt", allow_subset_of_names=False)
+ no_verify_ssl=False,
+ config_dir="/etc/letsencrypt",
+ work_dir="/var/lib/letsencrypt",
+ allow_subset_of_names=False)
+
# pylint: disable=star-args
self.account = mock.MagicMock(**{"key.pem": KEY})
- self.eg_domains = ["example.com", "www.example.com"]
from certbot.client import Client
with mock.patch("certbot.client.acme_client.Client") as acme:
@@ -120,6 +123,16 @@ class ClientTest(unittest.TestCase):
config=self.config, account_=self.account,
auth=None, installer=None)
+
+class ClientTest(ClientTestCommon):
+ """Tests for certbot.client.Client."""
+ def setUp(self):
+ super(ClientTest, self).setUp()
+
+ self.config.allow_subset_of_names = False
+ self.config.config_dir = "/etc/letsencrypt"
+ self.eg_domains = ["example.com", "www.example.com"]
+
def test_init_acme_verify_ssl(self):
net = self.acme_client.call_args[1]["net"]
self.assertTrue(net.verify_ssl)
@@ -213,6 +226,27 @@ class ClientTest(unittest.TestCase):
mock.sentinel.key, domains, self.config.csr_dir)
self._check_obtain_certificate()
+ @mock.patch('certbot.client.Client.obtain_certificate')
+ @mock.patch('certbot.storage.RenewableCert.new_lineage')
+ @mock.patch('OpenSSL.crypto.dump_certificate')
+ def test_obtain_and_enroll_certificate(self, mock_dump_certificate,
+ mock_storage, mock_obtain_certificate):
+ domains = ["example.com", "www.example.com"]
+ mock_obtain_certificate.return_value = (mock.MagicMock(),
+ mock.MagicMock(), mock.MagicMock(), None)
+
+ self.client.config.dry_run = False
+ self.assertTrue(self.client.obtain_and_enroll_certificate(domains, "example_cert"))
+
+ self.assertTrue(self.client.obtain_and_enroll_certificate(domains, None))
+
+ self.client.config.dry_run = True
+
+ self.assertFalse(self.client.obtain_and_enroll_certificate(domains, None))
+
+ self.assertTrue(mock_storage.call_count == 2)
+ self.assertTrue(mock_dump_certificate.call_count == 2)
+
@mock.patch("certbot.cli.helpful_parser")
def test_save_certificate(self, mock_parser):
# pylint: disable=too-many-locals
@@ -289,7 +323,7 @@ class ClientTest(unittest.TestCase):
["foo.bar"], "key", "cert", "chain", "fullchain")
installer.recovery_routine.assert_called_once_with()
- @mock.patch("certbot.client.zope.component.getUtility")
+ @test_util.patch_get_utility()
def test_deploy_certificate_restart_failure(self, mock_get_utility):
installer = mock.MagicMock()
installer.restart.side_effect = [errors.PluginError, None]
@@ -301,7 +335,7 @@ class ClientTest(unittest.TestCase):
installer.rollback_checkpoints.assert_called_once_with()
self.assertEqual(installer.restart.call_count, 2)
- @mock.patch("certbot.client.zope.component.getUtility")
+ @test_util.patch_get_utility()
def test_deploy_certificate_restart_failure2(self, mock_get_utility):
installer = mock.MagicMock()
installer.restart.side_effect = errors.PluginError
@@ -314,147 +348,109 @@ class ClientTest(unittest.TestCase):
installer.rollback_checkpoints.assert_called_once_with()
self.assertEqual(installer.restart.call_count, 1)
- @mock.patch("certbot.client.enhancements")
- def test_enhance_config(self, mock_enhancements):
- config = ConfigHelper(redirect=True, hsts=False, uir=False)
- self.assertRaises(errors.Error, self.client.enhance_config,
- ["foo.bar"], config, None)
- mock_enhancements.ask.return_value = True
- installer = mock.MagicMock()
- self.client.installer = installer
- installer.supported_enhancements.return_value = ["redirect"]
+class EnhanceConfigTest(ClientTestCommon):
+ """Tests for certbot.client.Client.enhance_config."""
+ def setUp(self):
+ super(EnhanceConfigTest, self).setUp()
- self.client.enhance_config(["foo.bar"], config, None)
- installer.enhance.assert_called_once_with("foo.bar", "redirect", None)
- self.assertEqual(installer.save.call_count, 1)
- installer.restart.assert_called_once_with()
+ self.config.hsts = False
+ self.config.redirect = False
+ self.config.staple = False
+ self.config.uir = False
+ self.domain = "example.org"
+
+ def test_no_installer(self):
+ self.assertRaises(
+ errors.Error, self.client.enhance_config, [self.domain], None)
@mock.patch("certbot.client.enhancements")
- def test_enhance_config_no_ask(self, mock_enhancements):
- config = ConfigHelper(redirect=True, hsts=False,
- uir=False, staple=False)
- self.assertRaises(errors.Error, self.client.enhance_config,
- ["foo.bar"], config, None)
+ def test_unsupported(self, mock_enhancements):
+ self.client.installer = mock.MagicMock()
+ self.client.installer.supported_enhancements.return_value = []
- mock_enhancements.ask.return_value = True
- installer = mock.MagicMock()
- self.client.installer = installer
- installer.supported_enhancements.return_value = [
- "redirect", "ensure-http-header", "staple-ocsp"]
-
- config = ConfigHelper(redirect=True, hsts=False,
- uir=False, staple=False)
- self.client.enhance_config(["foo.bar"], config, None)
- installer.enhance.assert_called_with("foo.bar", "redirect", None)
-
- config = ConfigHelper(redirect=False, hsts=True,
- uir=False, staple=False)
- self.client.enhance_config(["foo.bar"], config, None)
- installer.enhance.assert_called_with("foo.bar", "ensure-http-header",
- "Strict-Transport-Security")
-
- config = ConfigHelper(redirect=False, hsts=False,
- uir=True, staple=False)
- self.client.enhance_config(["foo.bar"], config, None)
- installer.enhance.assert_called_with("foo.bar", "ensure-http-header",
- "Upgrade-Insecure-Requests")
-
- config = ConfigHelper(redirect=False, hsts=False,
- uir=False, staple=True)
- self.client.enhance_config(["foo.bar"], config, None)
- installer.enhance.assert_called_with("foo.bar", "staple-ocsp", None)
-
- self.assertEqual(installer.save.call_count, 4)
- self.assertEqual(installer.restart.call_count, 4)
-
- @mock.patch("certbot.client.enhancements")
- def test_enhance_config_unsupported(self, mock_enhancements):
- installer = mock.MagicMock()
- self.client.installer = installer
- installer.supported_enhancements.return_value = []
-
- config = ConfigHelper(redirect=None, hsts=True, uir=True)
- self.client.enhance_config(["foo.bar"], config, None)
- installer.enhance.assert_not_called()
+ self.config.redirect = None
+ self.config.hsts = True
+ with mock.patch("certbot.client.logger") as mock_logger:
+ self.client.enhance_config([self.domain], None)
+ self.assertEqual(mock_logger.warning.call_count, 1)
+ self.client.installer.enhance.assert_not_called()
mock_enhancements.ask.assert_not_called()
- def test_enhance_config_no_installer(self):
- config = ConfigHelper(redirect=True, hsts=False, uir=False)
- self.assertRaises(errors.Error, self.client.enhance_config,
- ["foo.bar"], config, None)
+ def test_no_ask_hsts(self):
+ self.config.hsts = True
+ self._test_with_all_supported()
+ self.client.installer.enhance.assert_called_with(
+ self.domain, "ensure-http-header", "Strict-Transport-Security")
- @mock.patch("certbot.client.zope.component.getUtility")
- @mock.patch("certbot.client.enhancements")
- def test_enhance_config_enhance_failure(self, mock_enhancements,
- mock_get_utility):
- mock_enhancements.ask.return_value = True
+ def test_no_ask_redirect(self):
+ self.config.redirect = True
+ self._test_with_all_supported()
+ self.client.installer.enhance.assert_called_with(
+ self.domain, "redirect", None)
+
+ def test_no_ask_staple(self):
+ self.config.staple = True
+ self._test_with_all_supported()
+ self.client.installer.enhance.assert_called_with(
+ self.domain, "staple-ocsp", None)
+
+ def test_no_ask_uir(self):
+ self.config.uir = True
+ self._test_with_all_supported()
+ self.client.installer.enhance.assert_called_with(
+ self.domain, "ensure-http-header", "Upgrade-Insecure-Requests")
+
+ def test_enhance_failure(self):
+ self.client.installer = mock.MagicMock()
+ self.client.installer.enhance.side_effect = errors.PluginError
+ self._test_error()
+ self.client.installer.recovery_routine.assert_called_once_with()
+
+ def test_save_failure(self):
+ self.client.installer = mock.MagicMock()
+ self.client.installer.save.side_effect = errors.PluginError
+ self._test_error()
+ self.client.installer.recovery_routine.assert_called_once_with()
+ self.client.installer.save.assert_called_once_with(mock.ANY)
+
+ def test_restart_failure(self):
+ self.client.installer = mock.MagicMock()
+ self.client.installer.restart.side_effect = [errors.PluginError, None]
+ self._test_error_with_rollback()
+
+ def test_restart_failure2(self):
installer = mock.MagicMock()
- self.client.installer = installer
- installer.supported_enhancements.return_value = ["redirect"]
- installer.enhance.side_effect = errors.PluginError
-
- config = ConfigHelper(redirect=True, hsts=False, uir=False)
-
- self.assertRaises(errors.PluginError, self.client.enhance_config,
- ["foo.bar"], config, None)
- installer.recovery_routine.assert_called_once_with()
- self.assertEqual(mock_get_utility().add_message.call_count, 1)
-
- @mock.patch("certbot.client.zope.component.getUtility")
- @mock.patch("certbot.client.enhancements")
- def test_enhance_config_save_failure(self, mock_enhancements,
- mock_get_utility):
- mock_enhancements.ask.return_value = True
- installer = mock.MagicMock()
- self.client.installer = installer
- installer.supported_enhancements.return_value = ["redirect"]
- installer.save.side_effect = errors.PluginError
-
- config = ConfigHelper(redirect=True, hsts=False, uir=False)
-
- self.assertRaises(errors.PluginError, self.client.enhance_config,
- ["foo.bar"], config, None)
- installer.recovery_routine.assert_called_once_with()
- self.assertEqual(mock_get_utility().add_message.call_count, 1)
-
- @mock.patch("certbot.client.zope.component.getUtility")
- @mock.patch("certbot.client.enhancements")
- def test_enhance_config_restart_failure(self, mock_enhancements,
- mock_get_utility):
- mock_enhancements.ask.return_value = True
- installer = mock.MagicMock()
- self.client.installer = installer
- installer.supported_enhancements.return_value = ["redirect"]
- installer.restart.side_effect = [errors.PluginError, None]
-
- config = ConfigHelper(redirect=True, hsts=False, uir=False)
-
- self.assertRaises(errors.PluginError, self.client.enhance_config,
- ["foo.bar"], config, None)
-
- self.assertEqual(mock_get_utility().add_message.call_count, 1)
- installer.rollback_checkpoints.assert_called_once_with()
- self.assertEqual(installer.restart.call_count, 2)
-
- @mock.patch("certbot.client.zope.component.getUtility")
- @mock.patch("certbot.client.enhancements")
- def test_enhance_config_restart_failure2(self, mock_enhancements,
- mock_get_utility):
- mock_enhancements.ask.return_value = True
- installer = mock.MagicMock()
- self.client.installer = installer
- installer.supported_enhancements.return_value = ["redirect"]
installer.restart.side_effect = errors.PluginError
installer.rollback_checkpoints.side_effect = errors.ReverterError
+ self.client.installer = installer
+ self._test_error_with_rollback()
- config = ConfigHelper(redirect=True, hsts=False, uir=False)
+ @mock.patch("certbot.client.enhancements.ask")
+ def test_ask(self, mock_ask):
+ self.config.redirect = None
+ mock_ask.return_value = True
+ self._test_with_all_supported()
- self.assertRaises(errors.PluginError, self.client.enhance_config,
- ["foo.bar"], config, None)
- self.assertEqual(mock_get_utility().add_message.call_count, 1)
- installer.rollback_checkpoints.assert_called_once_with()
- self.assertEqual(installer.restart.call_count, 1)
+ def _test_error_with_rollback(self):
+ self._test_error()
+ self.assertTrue(self.client.installer.restart.called)
+
+ def _test_error(self):
+ self.config.redirect = True
+ with test_util.patch_get_utility() as mock_gu:
+ self.assertRaises(
+ errors.PluginError, self._test_with_all_supported)
+ self.assertEqual(mock_gu().add_message.call_count, 1)
+
+ def _test_with_all_supported(self):
+ if self.client.installer is None:
+ self.client.installer = mock.MagicMock()
+ self.client.installer.supported_enhancements.return_value = [
+ "ensure-http-header", "redirect", "staple-ocsp"]
+ self.client.enhance_config([self.domain], None)
+ self.assertEqual(self.client.installer.save.call_count, 1)
+ self.assertEqual(self.client.installer.restart.call_count, 1)
class RollbackTest(unittest.TestCase):
diff --git a/certbot/tests/configuration_test.py b/certbot/tests/configuration_test.py
index 211a0eae6..183d6a95c 100644
--- a/certbot/tests/configuration_test.py
+++ b/certbot/tests/configuration_test.py
@@ -88,31 +88,19 @@ class NamespaceConfigTest(unittest.TestCase):
self.assertTrue(os.path.isabs(config.key_dir))
self.assertTrue(os.path.isabs(config.temp_checkpoint_dir))
-
-class RenewerConfigurationTest(unittest.TestCase):
- """Test for certbot.configuration.RenewerConfiguration."""
-
- def setUp(self):
- self.namespace = mock.MagicMock(config_dir='/tmp/config')
- from certbot.configuration import RenewerConfiguration
- self.config = RenewerConfiguration(self.namespace)
-
@mock.patch('certbot.configuration.constants')
- def test_dynamic_dirs(self, constants):
+ def test_renewal_dynamic_dirs(self, constants):
constants.ARCHIVE_DIR = 'a'
constants.LIVE_DIR = 'l'
constants.RENEWAL_CONFIGS_DIR = 'renewal_configs'
- constants.RENEWER_CONFIG_FILENAME = 'r.conf'
- self.assertEqual(self.config.archive_dir, '/tmp/config/a')
+ self.assertEqual(self.config.default_archive_dir, '/tmp/config/a')
self.assertEqual(self.config.live_dir, '/tmp/config/l')
self.assertEqual(
self.config.renewal_configs_dir, '/tmp/config/renewal_configs')
- self.assertEqual(self.config.renewer_config_file, '/tmp/config/r.conf')
- def test_absolute_paths(self):
+ def test_renewal_absolute_paths(self):
from certbot.configuration import NamespaceConfig
- from certbot.configuration import RenewerConfiguration
config_base = "foo"
work_base = "bar"
@@ -125,12 +113,11 @@ class RenewerConfigurationTest(unittest.TestCase):
mock_namespace.config_dir = config_base
mock_namespace.work_dir = work_base
mock_namespace.logs_dir = logs_base
- config = RenewerConfiguration(NamespaceConfig(mock_namespace))
+ config = NamespaceConfig(mock_namespace)
- self.assertTrue(os.path.isabs(config.archive_dir))
+ self.assertTrue(os.path.isabs(config.default_archive_dir))
self.assertTrue(os.path.isabs(config.live_dir))
self.assertTrue(os.path.isabs(config.renewal_configs_dir))
- self.assertTrue(os.path.isabs(config.renewer_config_file))
if __name__ == '__main__':
diff --git a/certbot/tests/crypto_util_test.py b/certbot/tests/crypto_util_test.py
index c0dc1de3a..a580574a4 100644
--- a/certbot/tests/crypto_util_test.py
+++ b/certbot/tests/crypto_util_test.py
@@ -11,7 +11,7 @@ import zope.component
from certbot import errors
from certbot import interfaces
from certbot import util
-from certbot.tests import test_util
+import certbot.tests.util as test_util
RSA256_KEY = test_util.load_vector('rsa256_key.pem')
@@ -40,9 +40,9 @@ class InitSaveKeyTest(unittest.TestCase):
@mock.patch('certbot.crypto_util.make_key')
def test_success(self, mock_make):
- mock_make.return_value = 'key_pem'
+ mock_make.return_value = b'key_pem'
key = self._call(1024, self.key_dir)
- self.assertEqual(key.pem, 'key_pem')
+ self.assertEqual(key.pem, b'key_pem')
self.assertTrue('key-certbot.pem' in key.file)
@mock.patch('certbot.crypto_util.make_key')
@@ -67,13 +67,13 @@ class InitSaveCSRTest(unittest.TestCase):
def test_it(self, unused_mock_verify, mock_csr):
from certbot.crypto_util import init_save_csr
- mock_csr.return_value = ('csr_pem', 'csr_der')
+ mock_csr.return_value = (b'csr_pem', b'csr_der')
csr = init_save_csr(
mock.Mock(pem='dummy_key'), 'example.com', self.csr_dir,
'csr-certbot.pem')
- self.assertEqual(csr.data, 'csr_der')
+ self.assertEqual(csr.data, b'csr_der')
self.assertTrue('csr-certbot.pem' in csr.file)
diff --git a/certbot/tests/display/ops_test.py b/certbot/tests/display/ops_test.py
index bc0696f9c..c2f5d302f 100644
--- a/certbot/tests/display/ops_test.py
+++ b/certbot/tests/display/ops_test.py
@@ -13,11 +13,10 @@ from acme import messages
from certbot import account
from certbot import errors
-from certbot import interfaces
from certbot.display import util as display_util
-from certbot.tests import test_util
+import certbot.tests.util as test_util
KEY = jose.JWKRSA.load(test_util.load_vector("rsa512_key.pem"))
@@ -26,57 +25,73 @@ KEY = jose.JWKRSA.load(test_util.load_vector("rsa512_key.pem"))
class GetEmailTest(unittest.TestCase):
"""Tests for certbot.display.ops.get_email."""
- def setUp(self):
- mock_display = mock.MagicMock()
- self.input = mock_display.input
- zope.component.provideUtility(mock_display, interfaces.IDisplay)
-
@classmethod
def _call(cls, **kwargs):
from certbot.display.ops import get_email
return get_email(**kwargs)
- def test_cancel_none(self):
- self.input.return_value = (display_util.CANCEL, "foo@bar.baz")
+ @test_util.patch_get_utility("certbot.display.ops.z_util")
+ def test_cancel_none(self, mock_get_utility):
+ mock_input = mock_get_utility().input
+ mock_input.return_value = (display_util.CANCEL, "foo@bar.baz")
self.assertRaises(errors.Error, self._call)
self.assertRaises(errors.Error, self._call, optional=False)
- def test_ok_safe(self):
- self.input.return_value = (display_util.OK, "foo@bar.baz")
+ @test_util.patch_get_utility("certbot.display.ops.z_util")
+ def test_ok_safe(self, mock_get_utility):
+ mock_input = mock_get_utility().input
+ mock_input.return_value = (display_util.OK, "foo@bar.baz")
with mock.patch("certbot.display.ops.util.safe_email") as mock_safe_email:
mock_safe_email.return_value = True
self.assertTrue(self._call() is "foo@bar.baz")
- def test_ok_not_safe(self):
- self.input.return_value = (display_util.OK, "foo@bar.baz")
+ @test_util.patch_get_utility("certbot.display.ops.z_util")
+ def test_ok_not_safe(self, mock_get_utility):
+ mock_input = mock_get_utility().input
+ mock_input.return_value = (display_util.OK, "foo@bar.baz")
with mock.patch("certbot.display.ops.util.safe_email") as mock_safe_email:
mock_safe_email.side_effect = [False, True]
self.assertTrue(self._call() is "foo@bar.baz")
- def test_invalid_flag(self):
+ @test_util.patch_get_utility("certbot.display.ops.z_util")
+ def test_invalid_flag(self, mock_get_utility):
invalid_txt = "There seem to be problems"
- self.input.return_value = (display_util.OK, "foo@bar.baz")
+ mock_input = mock_get_utility().input
+ mock_input.return_value = (display_util.OK, "foo@bar.baz")
with mock.patch("certbot.display.ops.util.safe_email") as mock_safe_email:
mock_safe_email.return_value = True
self._call()
- self.assertTrue(invalid_txt not in self.input.call_args[0][0])
+ self.assertTrue(invalid_txt not in mock_input.call_args[0][0])
self._call(invalid=True)
- self.assertTrue(invalid_txt in self.input.call_args[0][0])
+ self.assertTrue(invalid_txt in mock_input.call_args[0][0])
- def test_optional_flag(self):
- self.input.return_value = (display_util.OK, "foo@bar.baz")
+ @test_util.patch_get_utility("certbot.display.ops.z_util")
+ def test_optional_flag(self, mock_get_utility):
+ mock_input = mock_get_utility().input
+ mock_input.return_value = (display_util.OK, "foo@bar.baz")
with mock.patch("certbot.display.ops.util.safe_email") as mock_safe_email:
mock_safe_email.side_effect = [False, True]
self._call(optional=False)
- for call in self.input.call_args_list:
+ for call in mock_input.call_args_list:
self.assertTrue(
"--register-unsafely-without-email" not in call[0][0])
+ @test_util.patch_get_utility("certbot.display.ops.z_util")
+ def test_optional_invalid_unsafe(self, mock_get_utility):
+ invalid_txt = "There seem to be problems"
+ mock_input = mock_get_utility().input
+ mock_input.return_value = (display_util.OK, "foo@bar.baz")
+ with mock.patch("certbot.display.ops.util.safe_email") as mock_safe_email:
+ mock_safe_email.side_effect = [False, True]
+ self._call(invalid=True)
+ self.assertTrue(invalid_txt in mock_input.call_args[0][0])
+
class ChooseAccountTest(unittest.TestCase):
"""Tests for certbot.display.ops.choose_account."""
def setUp(self):
- zope.component.provideUtility(display_util.FileDisplay(sys.stdout))
+ zope.component.provideUtility(display_util.FileDisplay(sys.stdout,
+ False))
self.accounts_dir = tempfile.mkdtemp("accounts")
self.account_keys_dir = os.path.join(self.accounts_dir, "keys")
@@ -119,7 +134,8 @@ class ChooseAccountTest(unittest.TestCase):
class GenSSLLabURLs(unittest.TestCase):
"""Loose test of _gen_ssl_lab_urls. URL can change easily in the future."""
def setUp(self):
- zope.component.provideUtility(display_util.FileDisplay(sys.stdout))
+ zope.component.provideUtility(display_util.FileDisplay(sys.stdout,
+ False))
@classmethod
def _call(cls, domains):
@@ -138,7 +154,8 @@ class GenSSLLabURLs(unittest.TestCase):
class GenHttpsNamesTest(unittest.TestCase):
"""Test _gen_https_names."""
def setUp(self):
- zope.component.provideUtility(display_util.FileDisplay(sys.stdout))
+ zope.component.provideUtility(display_util.FileDisplay(sys.stdout,
+ False))
@classmethod
def _call(cls, domains):
@@ -185,7 +202,8 @@ class GenHttpsNamesTest(unittest.TestCase):
class ChooseNamesTest(unittest.TestCase):
"""Test choose names."""
def setUp(self):
- zope.component.provideUtility(display_util.FileDisplay(sys.stdout))
+ zope.component.provideUtility(display_util.FileDisplay(sys.stdout,
+ False))
self.mock_install = mock.MagicMock()
@classmethod
@@ -215,6 +233,45 @@ class ChooseNamesTest(unittest.TestCase):
self.assertTrue(
"configuration files" in mock_util().input.call_args[0][0])
+ def test_sort_names_trivial(self):
+ from certbot.display.ops import _sort_names
+
+ #sort an empty list
+ self.assertEqual(_sort_names([]), [])
+
+ #sort simple domains
+ some_domains = ["ex.com", "zx.com", "ax.com"]
+ self.assertEqual(_sort_names(some_domains), ["ax.com", "ex.com", "zx.com"])
+
+ #Sort subdomains of a single domain
+ domain = ".ex.com"
+ unsorted_short = ["e", "a", "z", "y"]
+ unsorted_long = [us + domain for us in unsorted_short]
+
+ sorted_short = sorted(unsorted_short)
+ sorted_long = [us + domain for us in sorted_short]
+
+ self.assertEqual(_sort_names(unsorted_long), sorted_long)
+
+ def test_sort_names_many(self):
+ from certbot.display.ops import _sort_names
+
+ unsorted_domains = [".cx.com", ".bx.com", ".ax.com", ".dx.com"]
+ unsorted_short = ["www", "bnother.long.subdomain", "a", "a.long.subdomain", "z", "b"]
+ #Of course sorted doesn't work here ;-)
+ sorted_short = ["a", "b", "a.long.subdomain", "bnother.long.subdomain", "www", "z"]
+
+ to_sort = []
+ for short in unsorted_short:
+ for domain in unsorted_domains:
+ to_sort.append(short+domain)
+ sortd = []
+ for domain in sorted(unsorted_domains):
+ for short in sorted_short:
+ sortd.append(short+domain)
+ self.assertEqual(_sort_names(to_sort), sortd)
+
+
@mock.patch("certbot.display.ops.z_util")
def test_filter_names_valid_return(self, mock_util):
self.mock_install.get_all_names.return_value = set(["example.com"])
@@ -313,7 +370,7 @@ class SuccessRenewalTest(unittest.TestCase):
@classmethod
def _call(cls, names):
from certbot.display.ops import success_renewal
- success_renewal(names, "renew")
+ success_renewal(names)
@mock.patch("certbot.display.ops.z_util")
def test_success_renewal(self, mock_util):
@@ -328,6 +385,21 @@ class SuccessRenewalTest(unittest.TestCase):
for name in names:
self.assertTrue(name in arg)
+class SuccessRevocationTest(unittest.TestCase):
+ # pylint: disable=too-few-public-methods
+ """Test the success revocation message."""
+ @classmethod
+ def _call(cls, path):
+ from certbot.display.ops import success_revocation
+ success_revocation(path)
+
+ @mock.patch("certbot.display.ops.z_util")
+ def test_success_revocation(self, mock_util):
+ mock_util().notification.return_value = None
+ path = "/path/to/cert.pem"
+ self._call(path)
+ mock_util().notification.assert_called_once()
+ self.assertTrue(path in mock_util().notification.call_args[0][0])
if __name__ == "__main__":
unittest.main() # pragma: no cover
diff --git a/certbot/tests/display/util_test.py b/certbot/tests/display/util_test.py
index c1d33eff4..f4d69b50d 100644
--- a/certbot/tests/display/util_test.py
+++ b/certbot/tests/display/util_test.py
@@ -1,10 +1,12 @@
"""Test :mod:`certbot.display.util`."""
+import inspect
import os
import unittest
import mock
-import certbot.errors as errors
+from certbot import errors
+from certbot import interfaces
from certbot.display import util as display_util
@@ -13,122 +15,6 @@ CHOICES = [("First", "Description1"), ("Second", "Description2")]
TAGS = ["tag1", "tag2", "tag3"]
TAGS_CHOICES = [("1", "tag1"), ("2", "tag2"), ("3", "tag3")]
-
-class NcursesDisplayTest(unittest.TestCase):
- """Test ncurses display.
-
- Since this is mostly a wrapper, it might be more helpful to test the
- actual dialog boxes. The test file located in ./tests/display.py
- (relative to the root of the repository) will actually display the
- various boxes but requires the user to do the verification. If
- something seems amiss please use that test script to debug it, the
- automatic tests rely on too much mocking.
-
- """
- def setUp(self):
- super(NcursesDisplayTest, self).setUp()
- self.displayer = display_util.NcursesDisplay()
-
- self.default_menu_options = {
- "choices": CHOICES,
- "ok_label": "OK",
- "cancel_label": "Cancel",
- "help_button": False,
- "help_label": "",
- "width": display_util.WIDTH,
- "height": display_util.HEIGHT,
- "menu_height": display_util.HEIGHT - 6,
- }
-
- @mock.patch("certbot.display.util.dialog.Dialog.msgbox")
- def test_notification(self, mock_msgbox):
- """Kind of worthless... one liner."""
- self.displayer.notification("message")
- self.assertEqual(mock_msgbox.call_count, 1)
-
- @mock.patch("certbot.display.util.dialog.Dialog.menu")
- def test_menu_tag_and_desc(self, mock_menu):
- mock_menu.return_value = (display_util.OK, "First")
-
- ret = self.displayer.menu("Message", CHOICES)
- mock_menu.assert_called_with("Message", **self.default_menu_options)
-
- self.assertEqual(ret, (display_util.OK, 0))
-
- @mock.patch("certbot.display.util.dialog.Dialog.menu")
- def test_menu_tag_and_desc_cancel(self, mock_menu):
- mock_menu.return_value = (display_util.CANCEL, "")
-
- ret = self.displayer.menu("Message", CHOICES)
-
- mock_menu.assert_called_with("Message", **self.default_menu_options)
-
- self.assertEqual(ret, (display_util.CANCEL, -1))
-
- @mock.patch("certbot.display.util.dialog.Dialog.menu")
- def test_menu_desc_only(self, mock_menu):
- mock_menu.return_value = (display_util.OK, "1")
-
- ret = self.displayer.menu("Message", TAGS, help_label="More Info")
-
- self.default_menu_options.update(
- choices=TAGS_CHOICES, help_button=True, help_label="More Info")
- mock_menu.assert_called_with("Message", **self.default_menu_options)
-
- self.assertEqual(ret, (display_util.OK, 0))
-
- @mock.patch("certbot.display.util.dialog.Dialog.menu")
- def test_menu_desc_only_help(self, mock_menu):
- mock_menu.return_value = (display_util.HELP, "2")
-
- ret = self.displayer.menu("Message", TAGS, help_label="More Info")
-
- self.assertEqual(ret, (display_util.HELP, 1))
-
- @mock.patch("certbot.display.util.dialog.Dialog.menu")
- def test_menu_desc_only_cancel(self, mock_menu):
- mock_menu.return_value = (display_util.CANCEL, "")
-
- ret = self.displayer.menu("Message", TAGS, help_label="More Info")
-
- self.assertEqual(ret, (display_util.CANCEL, -1))
-
- @mock.patch("certbot.display.util."
- "dialog.Dialog.inputbox")
- def test_input(self, mock_input):
- mock_input.return_value = (mock.MagicMock(), mock.MagicMock())
- self.displayer.input("message")
- self.assertEqual(mock_input.call_count, 1)
-
- @mock.patch("certbot.display.util.dialog.Dialog.yesno")
- def test_yesno(self, mock_yesno):
- mock_yesno.return_value = display_util.OK
-
- self.assertTrue(self.displayer.yesno("message"))
-
- mock_yesno.assert_called_with(
- "message", yes_label="Yes", no_label="No")
-
- @mock.patch("certbot.display.util."
- "dialog.Dialog.checklist")
- def test_checklist(self, mock_checklist):
- mock_checklist.return_value = (mock.MagicMock(), mock.MagicMock())
- self.displayer.checklist("message", TAGS)
-
- choices = [
- (TAGS[0], "", True),
- (TAGS[1], "", True),
- (TAGS[2], "", True),
- ]
- mock_checklist.assert_called_with("message", choices=choices)
-
- @mock.patch("certbot.display.util.dialog.Dialog.dselect")
- def test_directory_select(self, mock_dselect):
- mock_dselect.return_value = (mock.MagicMock(), mock.MagicMock())
- self.displayer.directory_select("message")
- self.assertEqual(mock_dselect.call_count, 1)
-
-
class FileOutputDisplayTest(unittest.TestCase):
"""Test stdout display.
@@ -136,85 +22,149 @@ class FileOutputDisplayTest(unittest.TestCase):
functions look to a user, uncomment the test_visual function.
"""
+ # pylint:disable=too-many-public-methods
def setUp(self):
super(FileOutputDisplayTest, self).setUp()
self.mock_stdout = mock.MagicMock()
- self.displayer = display_util.FileDisplay(self.mock_stdout)
+ self.displayer = display_util.FileDisplay(self.mock_stdout, False)
def test_notification_no_pause(self):
- self.displayer.notification("message", 10, False)
+ self.displayer.notification("message", False)
string = self.mock_stdout.write.call_args[0][0]
self.assertTrue("message" in string)
def test_notification_pause(self):
with mock.patch("six.moves.input", return_value="enter"):
- self.displayer.notification("message")
+ self.displayer.notification("message", force_interactive=True)
self.assertTrue("message" in self.mock_stdout.write.call_args[0][0])
+ def test_notification_noninteractive(self):
+ self._force_noninteractive(self.displayer.notification, "message")
+ string = self.mock_stdout.write.call_args[0][0]
+ self.assertTrue("message" in string)
+
+ def test_notification_noninteractive2(self):
+ # The main purpose of this test is to make sure we only call
+ # logger.warning once which _force_noninteractive checks internally
+ self._force_noninteractive(self.displayer.notification, "message")
+ string = self.mock_stdout.write.call_args[0][0]
+ self.assertTrue("message" in string)
+
+ self.assertTrue(self.displayer.skipped_interaction)
+
+ self._force_noninteractive(self.displayer.notification, "message2")
+ string = self.mock_stdout.write.call_args[0][0]
+ self.assertTrue("message2" in string)
+
@mock.patch("certbot.display.util."
"FileDisplay._get_valid_int_ans")
def test_menu(self, mock_ans):
mock_ans.return_value = (display_util.OK, 1)
- ret = self.displayer.menu("message", CHOICES)
+ ret = self.displayer.menu("message", CHOICES, force_interactive=True)
self.assertEqual(ret, (display_util.OK, 0))
+ def test_menu_noninteractive(self):
+ default = 0
+ result = self._force_noninteractive(
+ self.displayer.menu, "msg", CHOICES, default=default)
+ self.assertEqual(result, (display_util.OK, default))
+
def test_input_cancel(self):
with mock.patch("six.moves.input", return_value="c"):
- code, _ = self.displayer.input("message")
+ code, _ = self.displayer.input("message", force_interactive=True)
self.assertTrue(code, display_util.CANCEL)
def test_input_normal(self):
with mock.patch("six.moves.input", return_value="domain.com"):
- code, input_ = self.displayer.input("message")
+ code, input_ = self.displayer.input("message", force_interactive=True)
self.assertEqual(code, display_util.OK)
self.assertEqual(input_, "domain.com")
+ def test_input_noninteractive(self):
+ default = "foo"
+ code, input_ = self._force_noninteractive(
+ self.displayer.input, "message", default=default)
+
+ self.assertEqual(code, display_util.OK)
+ self.assertEqual(input_, default)
+
+ def test_input_assertion_fail(self):
+ # If the call to util.assert_valid_call is commented out, an
+ # error.Error is raised, otherwise, an AssertionError is raised.
+ self.assertRaises(Exception, self._force_noninteractive,
+ self.displayer.input, "message", cli_flag="--flag")
+
+ def test_input_assertion_fail2(self):
+ with mock.patch("certbot.display.util.assert_valid_call"):
+ self.assertRaises(errors.Error, self._force_noninteractive,
+ self.displayer.input, "msg", cli_flag="--flag")
+
def test_yesno(self):
with mock.patch("six.moves.input", return_value="Yes"):
- self.assertTrue(self.displayer.yesno("message"))
+ self.assertTrue(self.displayer.yesno(
+ "message", force_interactive=True))
with mock.patch("six.moves.input", return_value="y"):
- self.assertTrue(self.displayer.yesno("message"))
+ self.assertTrue(self.displayer.yesno(
+ "message", force_interactive=True))
with mock.patch("six.moves.input", side_effect=["maybe", "y"]):
- self.assertTrue(self.displayer.yesno("message"))
+ self.assertTrue(self.displayer.yesno(
+ "message", force_interactive=True))
with mock.patch("six.moves.input", return_value="No"):
- self.assertFalse(self.displayer.yesno("message"))
+ self.assertFalse(self.displayer.yesno(
+ "message", force_interactive=True))
with mock.patch("six.moves.input", side_effect=["cancel", "n"]):
- self.assertFalse(self.displayer.yesno("message"))
+ self.assertFalse(self.displayer.yesno(
+ "message", force_interactive=True))
with mock.patch("six.moves.input", return_value="a"):
- self.assertTrue(self.displayer.yesno("msg", yes_label="Agree"))
+ self.assertTrue(self.displayer.yesno(
+ "msg", yes_label="Agree", force_interactive=True))
- @mock.patch("certbot.display.util.FileDisplay.input")
+ def test_yesno_noninteractive(self):
+ self.assertTrue(self._force_noninteractive(
+ self.displayer.yesno, "message", default=True))
+
+ @mock.patch("certbot.display.util.six.moves.input")
def test_checklist_valid(self, mock_input):
- mock_input.return_value = (display_util.OK, "2 1")
- code, tag_list = self.displayer.checklist("msg", TAGS)
+ mock_input.return_value = "2 1"
+ code, tag_list = self.displayer.checklist(
+ "msg", TAGS, force_interactive=True)
self.assertEqual(
(code, set(tag_list)), (display_util.OK, set(["tag1", "tag2"])))
- @mock.patch("certbot.display.util.FileDisplay.input")
- def test_checklist_miss_valid(self, mock_input):
- mock_input.side_effect = [
- (display_util.OK, "10"),
- (display_util.OK, "tag1 please"),
- (display_util.OK, "1")
- ]
+ @mock.patch("certbot.display.util.six.moves.input")
+ def test_checklist_empty(self, mock_input):
+ mock_input.return_value = ""
+ code, tag_list = self.displayer.checklist("msg", TAGS, force_interactive=True)
+ self.assertEqual(
+ (code, set(tag_list)), (display_util.OK, set(["tag1", "tag2", "tag3"])))
- ret = self.displayer.checklist("msg", TAGS)
+ @mock.patch("certbot.display.util.six.moves.input")
+ def test_checklist_miss_valid(self, mock_input):
+ mock_input.side_effect = ["10", "tag1 please", "1"]
+
+ ret = self.displayer.checklist("msg", TAGS, force_interactive=True)
self.assertEqual(ret, (display_util.OK, ["tag1"]))
- @mock.patch("certbot.display.util.FileDisplay.input")
+ @mock.patch("certbot.display.util.six.moves.input")
def test_checklist_miss_quit(self, mock_input):
- mock_input.side_effect = [
- (display_util.OK, "10"),
- (display_util.CANCEL, "1")
- ]
- ret = self.displayer.checklist("msg", TAGS)
+ mock_input.side_effect = ["10", "c"]
+
+ ret = self.displayer.checklist("msg", TAGS, force_interactive=True)
self.assertEqual(ret, (display_util.CANCEL, []))
+ def test_checklist_noninteractive(self):
+ default = TAGS
+ code, input_ = self._force_noninteractive(
+ self.displayer.checklist, "msg", TAGS, default=default)
+
+ self.assertEqual(code, display_util.OK)
+ self.assertEqual(input_, default)
+
def test_scrub_checklist_input_valid(self):
# pylint: disable=protected-access
indices = [
@@ -232,14 +182,38 @@ class FileOutputDisplayTest(unittest.TestCase):
self.displayer._scrub_checklist_input(list_, TAGS))
self.assertEqual(set_tags, exp[i])
- @mock.patch("certbot.display.util.FileDisplay.input")
+ @mock.patch("certbot.display.util.six.moves.input")
def test_directory_select(self, mock_input):
- message = "msg"
- result = (display_util.OK, "/var/www/html",)
- mock_input.return_value = result
+ # pylint: disable=star-args
+ args = ["msg", "/var/www/html", "--flag", True]
+ user_input = "/var/www/html"
+ mock_input.return_value = user_input
- self.assertEqual(self.displayer.directory_select(message), result)
- mock_input.assert_called_once_with(message)
+ returned = self.displayer.directory_select(*args)
+ self.assertEqual(returned, (display_util.OK, user_input))
+
+ def test_directory_select_noninteractive(self):
+ default = "/var/www/html"
+ code, input_ = self._force_noninteractive(
+ self.displayer.directory_select, "msg", default=default)
+
+ self.assertEqual(code, display_util.OK)
+ self.assertEqual(input_, default)
+
+ def _force_noninteractive(self, func, *args, **kwargs):
+ skipped_interaction = self.displayer.skipped_interaction
+
+ with mock.patch("certbot.display.util.sys.stdin") as mock_stdin:
+ mock_stdin.isatty.return_value = False
+ with mock.patch("certbot.display.util.logger") as mock_logger:
+ result = func(*args, **kwargs)
+
+ if skipped_interaction:
+ self.assertFalse(mock_logger.warning.called)
+ else:
+ self.assertEqual(mock_logger.warning.call_count, 1)
+
+ return result
def test_scrub_checklist_input_invalid(self):
# pylint: disable=protected-access
@@ -294,6 +268,13 @@ class FileOutputDisplayTest(unittest.TestCase):
self.displayer._get_valid_int_ans(3),
(display_util.CANCEL, -1))
+ def test_methods_take_force_interactive(self):
+ # Every IDisplay method implemented by FileDisplay must take
+ # force_interactive to prevent workflow regressions.
+ for name in interfaces.IDisplay.names(): # pylint: disable=no-member
+ arg_spec = inspect.getargspec(getattr(self.displayer, name))
+ self.assertTrue("force_interactive" in arg_spec.args)
+
class NoninteractiveDisplayTest(unittest.TestCase):
"""Test non-interactive display.
@@ -344,6 +325,16 @@ class NoninteractiveDisplayTest(unittest.TestCase):
self.assertRaises(
errors.MissingCommandlineFlag, self.displayer.directory_select, "msg")
+ def test_methods_take_kwargs(self):
+ # Every IDisplay method implemented by NoninteractiveDisplay
+ # should take **kwargs because every method of FileDisplay must
+ # take force_interactive which doesn't apply to
+ # NoninteractiveDisplay.
+ for name in interfaces.IDisplay.names(): # pylint: disable=no-member
+ method = getattr(self.displayer, name)
+ # asserts method accepts arbitrary keyword arguments
+ self.assertFalse(inspect.getargspec(method).keywords is None)
+
class SeparateListInputTest(unittest.TestCase):
"""Test Module functions."""
diff --git a/certbot/tests/error_handler_test.py b/certbot/tests/error_handler_test.py
index fa60d07b8..a548377bd 100644
--- a/certbot/tests/error_handler_test.py
+++ b/certbot/tests/error_handler_test.py
@@ -7,6 +7,7 @@ import unittest
import mock
+
def get_signals(signums):
"""Get the handlers for an iterable of signums."""
return dict((s, signal.getsignal(s)) for s in signums)
diff --git a/certbot/tests/hook_test.py b/certbot/tests/hook_test.py
index be7fb852d..87c86ad5c 100644
--- a/certbot/tests/hook_test.py
+++ b/certbot/tests/hook_test.py
@@ -27,53 +27,57 @@ 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._is_exe')
- def test_which(self, mock_is_exe):
- mock_is_exe.return_value = True
- self.assertEqual(hooks._which("/path/to/something"), "/path/to/something")
-
- with mock.patch.dict('os.environ', {"PATH": "/floop:/fleep"}):
- mock_is_exe.return_value = True
- self.assertEqual(hooks._which("pingify"), "/floop/pingify")
- mock_is_exe.return_value = False
- self.assertEqual(hooks._which("pingify"), None)
- self.assertEqual(hooks._which("/path/to/something"), None)
-
- @mock.patch('certbot.hooks._which')
- def test_prog(self, mockwhich):
- mockwhich.return_value = "/very/very/funky"
+ @mock.patch('certbot.hooks.util.exe_exists')
+ @mock.patch('certbot.hooks.plug_util.path_surgery')
+ def test_prog(self, mock_ps, mock_exe_exists):
+ mock_exe_exists.return_value = True
self.assertEqual(hooks._prog("funky"), "funky")
- mockwhich.return_value = None
+ self.assertEqual(mock_ps.call_count, 0)
+ mock_exe_exists.return_value = False
self.assertEqual(hooks._prog("funky"), None)
+ self.assertEqual(mock_ps.call_count, 1)
- def _test_a_hook(self, config, hook_function, calls_expected):
+ 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()
with mock.patch('certbot.hooks._run_hook') as mock_run_hook:
- hook_function(config)
- hook_function(config)
+ hook_function(config, **kwargs)
+ hook_function(config, **kwargs)
self.assertEqual(mock_run_hook.call_count, calls_expected)
return mock_logger.warning
def test_pre_hook(self):
- hooks.pre_hook.already = False
+ hooks.pre_hook.already = set()
config = mock.MagicMock(pre_hook="true")
self._test_a_hook(config, hooks.pre_hook, 1)
+ self._test_a_hook(config, hooks.pre_hook, 0)
+ config = mock.MagicMock(pre_hook="more_true")
+ self._test_a_hook(config, hooks.pre_hook, 1)
+ self._test_a_hook(config, hooks.pre_hook, 0)
config = mock.MagicMock(pre_hook="")
self._test_a_hook(config, hooks.pre_hook, 0)
- def test_post_hook(self):
- hooks.pre_hook.already = False
- # if pre-hook isn't called, post-hook shouldn't be
- config = mock.MagicMock(post_hook="true", verb="splonk")
- self._test_a_hook(config, hooks.post_hook, 0)
+ def _test_renew_post_hooks(self, expected_count):
+ with mock.patch('certbot.hooks.logger.info') as mock_info:
+ with mock.patch('certbot.hooks._run_hook') as mock_run:
+ hooks.run_saved_post_hooks()
+ self.assertEqual(mock_run.call_count, expected_count)
+ self.assertEqual(mock_info.call_count, expected_count)
+ def test_post_hooks(self):
config = mock.MagicMock(post_hook="true", verb="splonk")
- self._test_a_hook(config, hooks.pre_hook, 1)
self._test_a_hook(config, hooks.post_hook, 2)
+ self._test_renew_post_hooks(0)
config = mock.MagicMock(post_hook="true", verb="renew")
self._test_a_hook(config, hooks.post_hook, 0)
+ self._test_renew_post_hooks(1)
+ self._test_a_hook(config, hooks.post_hook, 0)
+ self._test_renew_post_hooks(1)
+
+ config = mock.MagicMock(post_hook="more_true", verb="renew")
+ self._test_a_hook(config, hooks.post_hook, 0)
+ self._test_renew_post_hooks(2)
def test_renew_hook(self):
with mock.patch.dict('os.environ', {}):
diff --git a/certbot/tests/log_test.py b/certbot/tests/log_test.py
deleted file mode 100644
index a4f394870..000000000
--- a/certbot/tests/log_test.py
+++ /dev/null
@@ -1,48 +0,0 @@
-"""Tests for certbot.log."""
-import logging
-import unittest
-
-import mock
-
-
-class DialogHandlerTest(unittest.TestCase):
-
- def setUp(self):
- self.d = mock.MagicMock()
-
- from certbot.log import DialogHandler
- self.handler = DialogHandler(height=2, width=6, d=self.d)
- self.handler.PADDING_HEIGHT = 2
- self.handler.PADDING_WIDTH = 4
-
- def test_adds_padding(self):
- self.handler.emit(logging.makeLogRecord({}))
- self.d.infobox.assert_called_once_with(mock.ANY, 4, 10)
-
- def test_args_in_msg_get_replaced(self):
- assert len('123456') <= self.handler.width
- self.handler.emit(logging.makeLogRecord(
- {'msg': '123%s', 'args': (456,)}))
- self.d.infobox.assert_called_once_with('123456', mock.ANY, mock.ANY)
-
- def test_wraps_nospace_is_greedy(self):
- assert len('1234567') > self.handler.width
- self.handler.emit(logging.makeLogRecord({'msg': '1234567'}))
- self.d.infobox.assert_called_once_with('123456\n7', mock.ANY, mock.ANY)
-
- def test_wraps_at_whitespace(self):
- assert len('123 567') > self.handler.width
- self.handler.emit(logging.makeLogRecord({'msg': '123 567'}))
- self.d.infobox.assert_called_once_with('123\n567', mock.ANY, mock.ANY)
-
- def test_only_last_lines_are_printed(self):
- assert len('a\nb\nc'.split()) > self.handler.height
- self.handler.emit(logging.makeLogRecord({'msg': 'a\n\nb\nc'}))
- self.d.infobox.assert_called_once_with('b\nc', mock.ANY, mock.ANY)
-
- def test_non_str(self):
- self.handler.emit(logging.makeLogRecord({'msg': {'foo': 'bar'}}))
-
-
-if __name__ == '__main__':
- unittest.main() # pragma: no cover
diff --git a/certbot/tests/main_test.py b/certbot/tests/main_test.py
index f7a6c5896..01ec2f061 100644
--- a/certbot/tests/main_test.py
+++ b/certbot/tests/main_test.py
@@ -1,66 +1,277 @@
"""Tests for certbot.main."""
+from __future__ import print_function
+
+import itertools
+import mock
import os
import shutil
import tempfile
+import traceback
import unittest
+import datetime
+import pytz
-import mock
+import six
+from acme import jose
+
+from certbot import account
from certbot import cli
from certbot import colored_logging
from certbot import constants
from certbot import configuration
+from certbot import crypto_util
from certbot import errors
-from certbot import log
-from certbot.plugins import disco as plugins_disco
+from certbot import main
+from certbot import renewal
+from certbot import storage
+from certbot import util
-class MainTest(unittest.TestCase):
- def setUp(self):
- pass
- def tearDown(self):
- pass
+from certbot.plugins import disco
+from certbot.plugins import manual
+import certbot.tests.util as test_util
+
+CERT_PATH = test_util.vector_path('cert.pem')
+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"))
+
+
+class TestHandleIdenticalCerts(unittest.TestCase):
+ """Test for certbot.main._handle_identical_cert_request"""
def test_handle_identical_cert_request_pending(self):
- from certbot import main
mock_lineage = mock.Mock()
mock_lineage.ensure_deployed.return_value = False
# pylint: disable=protected-access
ret = main._handle_identical_cert_request(mock.Mock(), mock_lineage)
self.assertEqual(ret, ("reinstall", mock_lineage))
+
+class RunTest(unittest.TestCase):
+ """Tests for certbot.main.run."""
+
+ def setUp(self):
+ self.domain = 'example.org'
+ self.patches = [
+ mock.patch('certbot.main._auth_from_available'),
+ mock.patch('certbot.main.display_ops.success_installation'),
+ mock.patch('certbot.main.display_ops.success_renewal'),
+ mock.patch('certbot.main._init_le_client'),
+ mock.patch('certbot.main._suggest_donation_if_appropriate')]
+
+ self.mock_auth = self.patches[0].start()
+ self.mock_success_installation = self.patches[1].start()
+ self.mock_success_renewal = self.patches[2].start()
+ self.mock_init = self.patches[3].start()
+ self.mock_suggest_donation = self.patches[4].start()
+
+ def tearDown(self):
+ for patch in self.patches:
+ patch.stop()
+
+ def _call(self):
+ args = '-a webroot -i null -d {0}'.format(self.domain).split()
+ plugins = disco.PluginsRegistry.find_all()
+ config = configuration.NamespaceConfig(
+ cli.prepare_and_parse_args(plugins, args))
+
+ from certbot.main import run
+ run(config, plugins)
+
+ def test_newcert_success(self):
+ self.mock_auth.return_value = ('newcert', mock.Mock())
+ self._call()
+ self.mock_success_installation.assert_called_once_with([self.domain])
+
+ def test_reinstall_success(self):
+ self.mock_auth.return_value = ('reinstall', mock.Mock())
+ self._call()
+ self.mock_success_installation.assert_called_once_with([self.domain])
+
+ def test_renewal_success(self):
+ self.mock_auth.return_value = ('renewal', mock.Mock())
+ self._call()
+ self.mock_success_renewal.assert_called_once_with([self.domain])
+
+
class ObtainCertTest(unittest.TestCase):
"""Tests for certbot.main.obtain_cert."""
def setUp(self):
- self.get_utility_patch = mock.patch(
- 'certbot.main.zope.component.getUtility')
+ self.get_utility_patch = test_util.patch_get_utility()
self.mock_get_utility = self.get_utility_patch.start()
def tearDown(self):
self.get_utility_patch.stop()
def _call(self, args):
- plugins = plugins_disco.PluginsRegistry.find_all()
+ plugins = disco.PluginsRegistry.find_all()
config = configuration.NamespaceConfig(
cli.prepare_and_parse_args(plugins, args))
- from certbot import main
with mock.patch('certbot.main._init_le_client') as mock_init:
main.obtain_cert(config, plugins)
return mock_init() # returns the client
- @mock.patch('certbot.main._auth_from_domains')
+ @mock.patch('certbot.main._auth_from_available')
def test_no_reinstall_text_pause(self, mock_auth):
mock_notification = self.mock_get_utility().notification
mock_notification.side_effect = self._assert_no_pause
mock_auth.return_value = ('reinstall', mock.ANY)
- self._call('certonly --webroot -d example.com -t'.split())
+ self._call('certonly --webroot -d example.com'.split())
- def _assert_no_pause(self, message, height=42, pause=True):
+ def _assert_no_pause(self, message, pause=True):
# pylint: disable=unused-argument
self.assertFalse(pause)
+ @mock.patch('certbot.cert_manager.lineage_for_certname')
+ @mock.patch('certbot.cert_manager.domains_for_certname')
+ @mock.patch('certbot.renewal.renew_cert')
+ @mock.patch('certbot.main._report_new_cert')
+ def test_find_lineage_for_domains_and_certname(self, mock_report_cert,
+ mock_renew_cert, mock_domains, mock_lineage):
+ domains = ['example.com', 'test.org']
+ mock_domains.return_value = domains
+ mock_lineage.names.return_value = domains
+ self._call(('certonly --webroot -d example.com -d test.org '
+ '--cert-name example.com').split())
+ self.assertTrue(mock_lineage.call_count == 1)
+ self.assertTrue(mock_domains.call_count == 1)
+ self.assertTrue(mock_renew_cert.call_count == 1)
+ self.assertTrue(mock_report_cert.call_count == 1)
+
+ # user confirms updating lineage with new domains
+ self._call(('certonly --webroot -d example.com -d test.com '
+ '--cert-name example.com').split())
+ self.assertTrue(mock_lineage.call_count == 2)
+ self.assertTrue(mock_domains.call_count == 2)
+ self.assertTrue(mock_renew_cert.call_count == 2)
+ self.assertTrue(mock_report_cert.call_count == 2)
+
+ # error in _ask_user_to_confirm_new_names
+ util_mock = mock.Mock()
+ util_mock.yesno.return_value = False
+ self.mock_get_utility.return_value = util_mock
+ self.assertRaises(errors.ConfigurationError, self._call,
+ ('certonly --webroot -d example.com -d test.com --cert-name example.com').split())
+
+ @mock.patch('certbot.cert_manager.domains_for_certname')
+ @mock.patch('certbot.display.ops.choose_names')
+ @mock.patch('certbot.cert_manager.lineage_for_certname')
+ @mock.patch('certbot.main._report_new_cert')
+ def test_find_lineage_for_domains_new_certname(self, mock_report_cert,
+ mock_lineage, mock_choose_names, mock_domains_for_certname):
+ mock_lineage.return_value = None
+
+ # no lineage with this name but we specified domains so create a new cert
+ self._call(('certonly --webroot -d example.com -d test.com '
+ '--cert-name example.com').split())
+ self.assertTrue(mock_lineage.call_count == 1)
+ self.assertTrue(mock_report_cert.call_count == 1)
+
+ # no lineage with this name and we didn't give domains
+ mock_choose_names.return_value = ["somename"]
+ mock_domains_for_certname.return_value = None
+ self._call(('certonly --webroot --cert-name example.com').split())
+ self.assertTrue(mock_choose_names.called)
+
+class FindDomainsOrCertnameTest(unittest.TestCase):
+ """Tests for certbot.main._find_domains_or_certname."""
+
+ @mock.patch('certbot.display.ops.choose_names')
+ def test_display_ops(self, mock_choose_names):
+ mock_config = mock.Mock(domains=None, certname=None)
+ mock_choose_names.return_value = "domainname"
+ # pylint: disable=protected-access
+ self.assertEqual(main._find_domains_or_certname(mock_config, None),
+ ("domainname", None))
+
+ @mock.patch('certbot.display.ops.choose_names')
+ def test_no_results(self, mock_choose_names):
+ mock_config = mock.Mock(domains=None, certname=None)
+ mock_choose_names.return_value = []
+ # pylint: disable=protected-access
+ self.assertRaises(errors.Error, main._find_domains_or_certname, mock_config, None)
+
+ @mock.patch('certbot.cert_manager.domains_for_certname')
+ def test_grab_domains(self, mock_domains):
+ mock_config = mock.Mock(domains=None, certname="one.com")
+ mock_domains.return_value = ["one.com", "two.com"]
+ # pylint: disable=protected-access
+ self.assertEqual(main._find_domains_or_certname(mock_config, None),
+ (["one.com", "two.com"], "one.com"))
+
+
+class RevokeTest(unittest.TestCase):
+ """Tests for certbot.main.revoke."""
+
+ def setUp(self):
+ self.tempdir_path = tempfile.mkdtemp()
+ shutil.copy(CERT_PATH, self.tempdir_path)
+ self.tmp_cert_path = os.path.abspath(os.path.join(self.tempdir_path,
+ 'cert.pem'))
+
+ self.patches = [
+ mock.patch('acme.client.Client', autospec=True),
+ mock.patch('certbot.client.Client'),
+ mock.patch('certbot.main._determine_account'),
+ mock.patch('certbot.main.display_ops.success_revocation')
+ ]
+ self.mock_acme_client = self.patches[0].start()
+ self.patches[1].start()
+ self.mock_determine_account = self.patches[2].start()
+ self.mock_success_revoke = self.patches[3].start()
+
+ from certbot.account import Account
+
+ self.regr = mock.MagicMock()
+ self.meta = Account.Meta(
+ creation_host="test.certbot.org",
+ creation_dt=datetime.datetime(
+ 2015, 7, 4, 14, 4, 10, tzinfo=pytz.UTC))
+ self.acc = Account(self.regr, JWK, self.meta)
+
+ self.mock_determine_account.return_value = (self.acc, None)
+
+ def tearDown(self):
+ shutil.rmtree(self.tempdir_path)
+ for patch in self.patches:
+ patch.stop()
+
+ def _call(self, extra_args=""):
+ args = 'revoke --cert-path={0} ' + extra_args
+ args = args.format(self.tmp_cert_path).split()
+ plugins = disco.PluginsRegistry.find_all()
+ config = configuration.NamespaceConfig(
+ cli.prepare_and_parse_args(plugins, args))
+
+ from certbot.main import revoke
+ revoke(config, plugins)
+
+ @mock.patch('certbot.main.client.acme_client')
+ def test_revoke_with_reason(self, mock_acme_client):
+ mock_revoke = mock_acme_client.Client().revoke
+ expected = []
+ for reason, code in constants.REVOCATION_REASONS.items():
+ self._call("--reason " + reason)
+ expected.append(mock.call(mock.ANY, code))
+ self._call("--reason " + reason.upper())
+ expected.append(mock.call(mock.ANY, code))
+ self.assertEqual(expected, mock_revoke.call_args_list)
+
+ def test_revocation_success(self):
+ self._call()
+ self.mock_success_revoke.assert_called_once_with(self.tmp_cert_path)
+
+ def test_revocation_error(self):
+ from acme import errors as acme_errors
+ self.mock_acme_client.side_effect = acme_errors.ClientError()
+ self.assertRaises(acme_errors.ClientError, self._call)
+ self.mock_success_revoke.assert_not_called()
+
class SetupLogFileHandlerTest(unittest.TestCase):
"""Tests for certbot.main.setup_log_file_handler."""
@@ -89,7 +300,7 @@ class SetupLoggingTest(unittest.TestCase):
def setUp(self):
self.config = mock.Mock(
logs_dir=tempfile.mkdtemp(),
- noninteractive_mode=False, quiet=False, text_mode=False,
+ noninteractive_mode=False, quiet=False,
verbose_count=constants.CLI_DEFAULTS['verbose_count'])
def tearDown(self):
@@ -107,7 +318,7 @@ class SetupLoggingTest(unittest.TestCase):
cli_handler = mock_get_logger().addHandler.call_args_list[0][0][0]
self.assertEqual(cli_handler.level, -self.config.verbose_count * 10)
self.assertTrue(
- isinstance(cli_handler, log.DialogHandler))
+ isinstance(cli_handler, colored_logging.StreamHandler))
@mock.patch('certbot.main.logging.getLogger')
def test_quiet_mode(self, mock_get_logger):
@@ -145,5 +356,866 @@ class MakeOrVerifyCoreDirTest(unittest.TestCase):
self.dir, 0o700, os.geteuid(), False)
+class DetermineAccountTest(unittest.TestCase):
+ """Tests for certbot.main._determine_account."""
+
+ def setUp(self):
+ self.args = mock.MagicMock(account=None, email=None,
+ register_unsafely_without_email=False)
+ self.config = configuration.NamespaceConfig(self.args)
+ self.accs = [mock.MagicMock(id='x'), mock.MagicMock(id='y')]
+ self.account_storage = account.AccountMemoryStorage()
+
+ def _call(self):
+ # pylint: disable=protected-access
+ from certbot.main import _determine_account
+ with mock.patch('certbot.main.account.AccountFileStorage') as mock_storage:
+ mock_storage.return_value = self.account_storage
+ return _determine_account(self.config)
+
+ def test_args_account_set(self):
+ self.account_storage.save(self.accs[1])
+ self.config.account = self.accs[1].id
+ self.assertEqual((self.accs[1], None), self._call())
+ self.assertEqual(self.accs[1].id, self.config.account)
+ self.assertTrue(self.config.email is None)
+
+ def test_single_account(self):
+ self.account_storage.save(self.accs[0])
+ self.assertEqual((self.accs[0], None), self._call())
+ self.assertEqual(self.accs[0].id, self.config.account)
+ self.assertTrue(self.config.email is None)
+
+ @mock.patch('certbot.client.display_ops.choose_account')
+ def test_multiple_accounts(self, mock_choose_accounts):
+ for acc in self.accs:
+ self.account_storage.save(acc)
+ mock_choose_accounts.return_value = self.accs[1]
+ self.assertEqual((self.accs[1], None), self._call())
+ self.assertEqual(
+ set(mock_choose_accounts.call_args[0][0]), set(self.accs))
+ self.assertEqual(self.accs[1].id, self.config.account)
+ self.assertTrue(self.config.email is None)
+
+ @mock.patch('certbot.client.display_ops.get_email')
+ def test_no_accounts_no_email(self, mock_get_email):
+ mock_get_email.return_value = 'foo@bar.baz'
+
+ with mock.patch('certbot.main.client') as client:
+ client.register.return_value = (
+ self.accs[0], mock.sentinel.acme)
+ self.assertEqual((self.accs[0], mock.sentinel.acme), self._call())
+ client.register.assert_called_once_with(
+ self.config, self.account_storage, tos_cb=mock.ANY)
+
+ self.assertEqual(self.accs[0].id, self.config.account)
+ self.assertEqual('foo@bar.baz', self.config.email)
+
+ def test_no_accounts_email(self):
+ self.config.email = 'other email'
+ with mock.patch('certbot.main.client') as client:
+ client.register.return_value = (self.accs[1], mock.sentinel.acme)
+ self._call()
+ self.assertEqual(self.accs[1].id, self.config.account)
+ self.assertEqual('other email', self.config.email)
+
+
+class MainTest(unittest.TestCase): # pylint: disable=too-many-public-methods
+ """Tests for different commands."""
+
+ def setUp(self):
+ self.tmp_dir = tempfile.mkdtemp()
+ self.config_dir = os.path.join(self.tmp_dir, 'config')
+ self.work_dir = os.path.join(self.tmp_dir, 'work')
+ self.logs_dir = os.path.join(self.tmp_dir, 'logs')
+ os.mkdir(self.logs_dir)
+ self.standard_args = ['--config-dir', self.config_dir,
+ '--work-dir', self.work_dir,
+ '--logs-dir', self.logs_dir, '--text']
+
+ def tearDown(self):
+ shutil.rmtree(self.tmp_dir)
+ # Reset globals in cli
+ # pylint: disable=protected-access
+ cli._parser = cli.set_by_cli.detector = None
+
+ def _call(self, args, stdout=None):
+ "Run the cli with output streams and actual client mocked out"
+ with mock.patch('certbot.main.client') as client:
+ ret, stdout, stderr = self._call_no_clientmock(args, stdout)
+ return ret, stdout, stderr, client
+
+ def _call_no_clientmock(self, args, stdout=None):
+ "Run the client with output streams mocked out"
+ args = self.standard_args + args
+
+ toy_stdout = stdout if stdout else six.StringIO()
+ with mock.patch('certbot.main.sys.stdout', new=toy_stdout):
+ with mock.patch('certbot.main.sys.stderr') as stderr:
+ ret = main.main(args[:]) # NOTE: parser can alter its args!
+ return ret, toy_stdout, stderr
+
+ def test_no_flags(self):
+ with mock.patch('certbot.main.run') as mock_run:
+ self._call([])
+ self.assertEqual(1, mock_run.call_count)
+
+ def test_version_string_program_name(self):
+ toy_out = six.StringIO()
+ toy_err = six.StringIO()
+ with mock.patch('certbot.main.sys.stdout', new=toy_out):
+ with mock.patch('certbot.main.sys.stderr', new=toy_err):
+ try:
+ main.main(["--version"])
+ except SystemExit:
+ pass
+ finally:
+ output = toy_out.getvalue() or toy_err.getvalue()
+ self.assertTrue("certbot" in output, "Output is {0}".format(output))
+ toy_out.close()
+ toy_err.close()
+
+ def _cli_missing_flag(self, args, message):
+ "Ensure that a particular error raises a missing cli flag error containing message"
+ exc = None
+ try:
+ with mock.patch('certbot.main.sys.stderr'):
+ main.main(self.standard_args + args[:]) # NOTE: parser can alter its args!
+ except errors.MissingCommandlineFlag as exc_:
+ exc = exc_
+ self.assertTrue(message in str(exc))
+ self.assertTrue(exc is not None)
+
+ def test_noninteractive(self):
+ args = ['-n', 'certonly']
+ self._cli_missing_flag(args, "specify a plugin")
+ args.extend(['--standalone', '-d', 'eg.is'])
+ self._cli_missing_flag(args, "register before running")
+ with mock.patch('certbot.main._auth_from_available'):
+ with mock.patch('certbot.main.client.acme_from_config_key'):
+ args.extend(['--email', 'io@io.is'])
+ self._cli_missing_flag(args, "--agree-tos")
+
+ @mock.patch('certbot.main.client.acme_client.Client')
+ @mock.patch('certbot.main._determine_account')
+ @mock.patch('certbot.main.client.Client.obtain_and_enroll_certificate')
+ @mock.patch('certbot.main._auth_from_available')
+ def test_user_agent(self, afa, _obt, det, _client):
+ # Normally the client is totally mocked out, but here we need more
+ # arguments to automate it...
+ args = ["--standalone", "certonly", "-m", "none@none.com",
+ "-d", "example.com", '--agree-tos'] + self.standard_args
+ det.return_value = mock.MagicMock(), None
+ afa.return_value = "newcert", mock.MagicMock()
+
+ with mock.patch('certbot.main.client.acme_client.ClientNetwork') as acme_net:
+ self._call_no_clientmock(args)
+ os_ver = util.get_os_info_ua()
+ ua = acme_net.call_args[1]["user_agent"]
+ self.assertTrue(os_ver in ua)
+ import platform
+ plat = platform.platform()
+ if "linux" in plat.lower():
+ self.assertTrue(util.get_os_info_ua() in ua)
+
+ with mock.patch('certbot.main.client.acme_client.ClientNetwork') as acme_net:
+ ua = "bandersnatch"
+ args += ["--user-agent", ua]
+ self._call_no_clientmock(args)
+ acme_net.assert_called_once_with(mock.ANY, verify_ssl=True, user_agent=ua)
+
+ @mock.patch('certbot.main.plug_sel.record_chosen_plugins')
+ @mock.patch('certbot.main.plug_sel.pick_installer')
+ def test_installer_selection(self, mock_pick_installer, _rec):
+ self._call(['install', '--domains', 'foo.bar', '--cert-path', 'cert',
+ '--key-path', 'key', '--chain-path', 'chain'])
+ self.assertEqual(mock_pick_installer.call_count, 1)
+
+ @mock.patch('certbot.util.exe_exists')
+ def test_configurator_selection(self, mock_exe_exists):
+ mock_exe_exists.return_value = True
+ real_plugins = disco.PluginsRegistry.find_all()
+ args = ['--apache', '--authenticator', 'standalone']
+
+ # This needed two calls to find_all(), which we're avoiding for now
+ # because of possible side effects:
+ # https://github.com/letsencrypt/letsencrypt/commit/51ed2b681f87b1eb29088dd48718a54f401e4855
+ #with mock.patch('certbot.cli.plugins_testable') as plugins:
+ # plugins.return_value = {"apache": True, "nginx": True}
+ # ret, _, _, _ = self._call(args)
+ # self.assertTrue("Too many flags setting" in ret)
+
+ args = ["install", "--nginx", "--cert-path", "/tmp/blah", "--key-path", "/tmp/blah",
+ "--nginx-server-root", "/nonexistent/thing", "-d",
+ "example.com", "--debug"]
+ if "nginx" in real_plugins:
+ # Sending nginx a non-existent conf dir will simulate misconfiguration
+ # (we can only do that if certbot-nginx is actually present)
+ ret, _, _, _ = self._call(args)
+ self.assertTrue("The nginx plugin is not working" in ret)
+ self.assertTrue("MisconfigurationError" in ret)
+
+ self._cli_missing_flag(["--standalone"], "With the standalone plugin, you probably")
+
+ with mock.patch("certbot.main._init_le_client") as mock_init:
+ with mock.patch("certbot.main._auth_from_available") as mock_afa:
+ mock_afa.return_value = (mock.MagicMock(), mock.MagicMock())
+ self._call(["certonly", "--manual", "-d", "foo.bar"])
+ unused_config, auth, unused_installer = mock_init.call_args[0]
+ self.assertTrue(isinstance(auth, manual.Authenticator))
+
+ with mock.patch('certbot.main.obtain_cert') as mock_certonly:
+ self._call(["auth", "--standalone"])
+ self.assertEqual(1, mock_certonly.call_count)
+
+ def test_rollback(self):
+ _, _, _, client = self._call(['rollback'])
+ self.assertEqual(1, client.rollback.call_count)
+
+ _, _, _, client = self._call(['rollback', '--checkpoints', '123'])
+ client.rollback.assert_called_once_with(
+ mock.ANY, 123, mock.ANY, mock.ANY)
+
+ def test_config_changes(self):
+ _, _, _, client = self._call(['config_changes'])
+ self.assertEqual(1, client.view_config_changes.call_count)
+
+ @mock.patch('certbot.cert_manager.update_live_symlinks')
+ def test_update_symlinks(self, mock_cert_manager):
+ self._call_no_clientmock(['update_symlinks'])
+ self.assertEqual(1, mock_cert_manager.call_count)
+
+ @mock.patch('certbot.cert_manager.certificates')
+ def test_certificates(self, mock_cert_manager):
+ self._call_no_clientmock(['certificates'])
+ self.assertEqual(1, mock_cert_manager.call_count)
+
+ @mock.patch('certbot.cert_manager.delete')
+ def test_delete(self, mock_cert_manager):
+ self._call_no_clientmock(['delete'])
+ self.assertEqual(1, mock_cert_manager.call_count)
+
+ def test_plugins(self):
+ flags = ['--init', '--prepare', '--authenticators', '--installers']
+ for args in itertools.chain(
+ *(itertools.combinations(flags, r)
+ for r in six.moves.range(len(flags)))):
+ self._call(['plugins'] + list(args))
+
+ @mock.patch('certbot.main.plugins_disco')
+ @mock.patch('certbot.main.cli.HelpfulArgumentParser.determine_help_topics')
+ def test_plugins_no_args(self, _det, mock_disco):
+ ifaces = []
+ plugins = mock_disco.PluginsRegistry.find_all()
+
+ _, stdout, _, _ = self._call(['plugins'])
+ plugins.visible.assert_called_once_with()
+ plugins.visible().ifaces.assert_called_once_with(ifaces)
+ filtered = plugins.visible().ifaces()
+ self.assertEqual(stdout.getvalue().strip(), str(filtered))
+
+ @mock.patch('certbot.main.plugins_disco')
+ @mock.patch('certbot.main.cli.HelpfulArgumentParser.determine_help_topics')
+ def test_plugins_init(self, _det, mock_disco):
+ ifaces = []
+ plugins = mock_disco.PluginsRegistry.find_all()
+
+ _, stdout, _, _ = self._call(['plugins', '--init'])
+ plugins.visible.assert_called_once_with()
+ plugins.visible().ifaces.assert_called_once_with(ifaces)
+ filtered = plugins.visible().ifaces()
+ self.assertEqual(filtered.init.call_count, 1)
+ filtered.verify.assert_called_once_with(ifaces)
+ verified = filtered.verify()
+ self.assertEqual(stdout.getvalue().strip(), str(verified))
+
+ @mock.patch('certbot.main.plugins_disco')
+ @mock.patch('certbot.main.cli.HelpfulArgumentParser.determine_help_topics')
+ def test_plugins_prepare(self, _det, mock_disco):
+ ifaces = []
+ plugins = mock_disco.PluginsRegistry.find_all()
+ _, stdout, _, _ = self._call(['plugins', '--init', '--prepare'])
+ plugins.visible.assert_called_once_with()
+ plugins.visible().ifaces.assert_called_once_with(ifaces)
+ filtered = plugins.visible().ifaces()
+ self.assertEqual(filtered.init.call_count, 1)
+ filtered.verify.assert_called_once_with(ifaces)
+ verified = filtered.verify()
+ verified.prepare.assert_called_once_with()
+ verified.available.assert_called_once_with()
+ available = verified.available()
+ self.assertEqual(stdout.getvalue().strip(), str(available))
+
+ def test_certonly_abspath(self):
+ cert = 'cert'
+ key = 'key'
+ chain = 'chain'
+ fullchain = 'fullchain'
+
+ with mock.patch('certbot.main.obtain_cert') as mock_obtaincert:
+ self._call(['certonly', '--cert-path', cert, '--key-path', 'key',
+ '--chain-path', 'chain',
+ '--fullchain-path', 'fullchain'])
+
+ config, unused_plugins = mock_obtaincert.call_args[0]
+ self.assertEqual(config.cert_path, os.path.abspath(cert))
+ self.assertEqual(config.key_path, os.path.abspath(key))
+ self.assertEqual(config.chain_path, os.path.abspath(chain))
+ self.assertEqual(config.fullchain_path, os.path.abspath(fullchain))
+
+ def test_certonly_bad_args(self):
+ try:
+ self._call(['-a', 'bad_auth', 'certonly'])
+ assert False, "Exception should have been raised"
+ except errors.PluginSelectionError as e:
+ self.assertTrue('The requested bad_auth plugin does not appear' in str(e))
+
+ def test_check_config_sanity_domain(self):
+ # FQDN
+ self.assertRaises(errors.ConfigurationError,
+ self._call,
+ ['-d', 'a' * 64])
+ # FQDN 2
+ self.assertRaises(errors.ConfigurationError,
+ self._call,
+ ['-d', (('a' * 50) + '.') * 10])
+ # Wildcard
+ self.assertRaises(errors.ConfigurationError,
+ self._call,
+ ['-d', '*.wildcard.tld'])
+
+ # Bare IP address (this is actually a different error message now)
+ self.assertRaises(errors.ConfigurationError,
+ self._call,
+ ['-d', '204.11.231.35'])
+
+ def test_csr_with_besteffort(self):
+ self.assertRaises(
+ errors.Error, self._call,
+ 'certonly --csr {0} --allow-subset-of-names'.format(CSR).split())
+
+ def test_run_with_csr(self):
+ # This is an error because you can only use --csr with certonly
+ try:
+ self._call(['--csr', CSR])
+ except errors.Error as e:
+ assert "Please try the certonly" in repr(e)
+ return
+ assert False, "Expected supplying --csr to fail with default verb"
+
+ def test_csr_with_no_domains(self):
+ self.assertRaises(
+ errors.Error, self._call,
+ 'certonly --csr {0}'.format(
+ test_util.vector_path('csr-nonames.pem')).split())
+
+ def test_csr_with_inconsistent_domains(self):
+ self.assertRaises(
+ errors.Error, self._call,
+ 'certonly -d example.org --csr {0}'.format(CSR).split())
+
+ def _certonly_new_request_common(self, mock_client, args=None):
+ with mock.patch('certbot.main._find_lineage_for_domains_and_certname') as mock_renewal:
+ mock_renewal.return_value = ("newcert", None)
+ with mock.patch('certbot.main._init_le_client') as mock_init:
+ mock_init.return_value = mock_client
+ if args is None:
+ args = []
+ args += '-d foo.bar -a standalone certonly'.split()
+ self._call(args)
+
+ @test_util.patch_get_utility()
+ def test_certonly_dry_run_new_request_success(self, mock_get_utility):
+ mock_client = mock.MagicMock()
+ mock_client.obtain_and_enroll_certificate.return_value = None
+ self._certonly_new_request_common(mock_client, ['--dry-run'])
+ self.assertEqual(
+ mock_client.obtain_and_enroll_certificate.call_count, 1)
+ self.assertTrue(
+ 'dry run' in mock_get_utility().add_message.call_args[0][0])
+ # Asserts we don't suggest donating after a successful dry run
+ self.assertEqual(mock_get_utility().add_message.call_count, 1)
+
+ @mock.patch('certbot.crypto_util.notAfter')
+ @test_util.patch_get_utility()
+ def test_certonly_new_request_success(self, mock_get_utility, mock_notAfter):
+ cert_path = '/etc/letsencrypt/live/foo.bar'
+ date = '1970-01-01'
+ mock_notAfter().date.return_value = date
+
+ mock_lineage = mock.MagicMock(cert=cert_path, fullchain=cert_path)
+ mock_client = mock.MagicMock()
+ mock_client.obtain_and_enroll_certificate.return_value = mock_lineage
+ self._certonly_new_request_common(mock_client)
+ self.assertEqual(
+ mock_client.obtain_and_enroll_certificate.call_count, 1)
+ 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(
+ 'donate' in mock_get_utility().add_message.call_args[0][0])
+
+ def test_certonly_new_request_failure(self):
+ mock_client = mock.MagicMock()
+ mock_client.obtain_and_enroll_certificate.return_value = False
+ self.assertRaises(errors.Error,
+ self._certonly_new_request_common, mock_client)
+
+ def _test_renewal_common(self, due_for_renewal, extra_args, log_out=None,
+ args=None, should_renew=True, error_expected=False):
+ # pylint: disable=too-many-locals,too-many-arguments
+ cert_path = test_util.vector_path('cert.pem')
+ chain_path = '/etc/letsencrypt/live/foo.bar/fullchain.pem'
+ mock_lineage = mock.MagicMock(cert=cert_path, fullchain=chain_path)
+ mock_lineage.should_autorenew.return_value = due_for_renewal
+ mock_lineage.has_pending_deployment.return_value = False
+ mock_lineage.names.return_value = ['isnot.org']
+ mock_certr = mock.MagicMock()
+ mock_key = mock.MagicMock(pem='pem_key')
+ mock_client = mock.MagicMock()
+ stdout = None
+ mock_client.obtain_certificate.return_value = (mock_certr, 'chain',
+ mock_key, 'csr')
+ try:
+ with mock.patch('certbot.cert_manager.find_duplicative_certs') as mock_fdc:
+ mock_fdc.return_value = (mock_lineage, None)
+ with mock.patch('certbot.main._init_le_client') as mock_init:
+ mock_init.return_value = mock_client
+ with test_util.patch_get_utility() as mock_get_utility:
+ with mock.patch('certbot.main.renewal.OpenSSL') as mock_ssl:
+ mock_latest = mock.MagicMock()
+ mock_latest.get_issuer.return_value = "Fake fake"
+ mock_ssl.crypto.load_certificate.return_value = mock_latest
+ with mock.patch('certbot.main.renewal.crypto_util'):
+ if not args:
+ args = ['-d', 'isnot.org', '-a', 'standalone', 'certonly']
+ if extra_args:
+ args += extra_args
+ try:
+ ret, stdout, _, _ = self._call(args)
+ if ret:
+ print("Returned", ret)
+ raise AssertionError(ret)
+ assert not error_expected, "renewal should have errored"
+ except: # pylint: disable=bare-except
+ if not error_expected:
+ raise AssertionError(
+ "Unexpected renewal error:\n" +
+ traceback.format_exc())
+
+ if should_renew:
+ mock_client.obtain_certificate.assert_called_once_with(['isnot.org'])
+ else:
+ self.assertEqual(mock_client.obtain_certificate.call_count, 0)
+ except:
+ self._dump_log()
+ raise
+ finally:
+ if log_out:
+ with open(os.path.join(self.logs_dir, "letsencrypt.log")) as lf:
+ self.assertTrue(log_out in lf.read())
+
+ return mock_lineage, mock_get_utility, stdout
+
+ def test_certonly_renewal(self):
+ lineage, get_utility, _ = self._test_renewal_common(True, [])
+ self.assertEqual(lineage.save_successor.call_count, 1)
+ lineage.update_all_links_to.assert_called_once_with(
+ lineage.latest_common_version())
+ cert_msg = get_utility().add_message.call_args_list[0][0][0]
+ self.assertTrue('fullchain.pem' in cert_msg)
+ self.assertTrue('donate' in get_utility().add_message.call_args[0][0])
+
+ def test_certonly_renewal_triggers(self):
+ # --dry-run should force renewal
+ _, get_utility, _ = self._test_renewal_common(False, ['--dry-run', '--keep'],
+ log_out="simulating renewal")
+ self.assertEqual(get_utility().add_message.call_count, 1)
+ self.assertTrue('dry run' in get_utility().add_message.call_args[0][0])
+
+ self._test_renewal_common(False, ['--renew-by-default', '-tvv', '--debug'],
+ log_out="Auto-renewal forced")
+ self.assertEqual(get_utility().add_message.call_count, 1)
+
+ self._test_renewal_common(False, ['-tvv', '--debug', '--keep'],
+ log_out="not yet due", should_renew=False)
+
+ def _dump_log(self):
+ print("Logs:")
+ log_path = os.path.join(self.logs_dir, "letsencrypt.log")
+ if os.path.exists(log_path):
+ with open(log_path) as lf:
+ print(lf.read())
+
+ def test_renew_verb(self):
+ test_util.make_lineage(self, 'sample-renewal.conf')
+ args = ["renew", "--dry-run", "-tvv"]
+ self._test_renewal_common(True, [], args=args, should_renew=True)
+
+ def test_quiet_renew(self):
+ test_util.make_lineage(self, 'sample-renewal.conf')
+ args = ["renew", "--dry-run"]
+ _, _, stdout = self._test_renewal_common(True, [], args=args, should_renew=True)
+ out = stdout.getvalue()
+ self.assertTrue("renew" in out)
+
+ args = ["renew", "--dry-run", "-q"]
+ _, _, stdout = self._test_renewal_common(True, [], args=args, should_renew=True)
+ out = stdout.getvalue()
+ self.assertEqual("", out)
+
+ def test_renew_hook_validation(self):
+ test_util.make_lineage(self, 'sample-renewal.conf')
+ args = ["renew", "--dry-run", "--post-hook=no-such-command"]
+ self._test_renewal_common(True, [], args=args, should_renew=False,
+ error_expected=True)
+
+ def test_renew_no_hook_validation(self):
+ test_util.make_lineage(self, 'sample-renewal.conf')
+ args = ["renew", "--dry-run", "--post-hook=no-such-command",
+ "--disable-hook-validation"]
+ self._test_renewal_common(True, [], args=args, should_renew=True,
+ error_expected=False)
+
+ @mock.patch("certbot.cli.set_by_cli")
+ def test_ancient_webroot_renewal_conf(self, mock_set_by_cli):
+ mock_set_by_cli.return_value = False
+ rc_path = test_util.make_lineage(self, 'sample-renewal-ancient.conf')
+ args = mock.MagicMock(account=None, email=None, webroot_path=None)
+ config = configuration.NamespaceConfig(args)
+ lineage = storage.RenewableCert(rc_path, config)
+ renewalparams = lineage.configuration["renewalparams"]
+ # pylint: disable=protected-access
+ renewal._restore_webroot_config(config, renewalparams)
+ self.assertEqual(config.webroot_path, ["/var/www/"])
+
+ def test_renew_verb_empty_config(self):
+ rd = os.path.join(self.config_dir, 'renewal')
+ if not os.path.exists(rd):
+ os.makedirs(rd)
+ with open(os.path.join(rd, 'empty.conf'), 'w'):
+ pass # leave the file empty
+ args = ["renew", "--dry-run", "-tvv"]
+ self._test_renewal_common(False, [], args=args, should_renew=False, error_expected=True)
+
+ def test_renew_with_certname(self):
+ test_util.make_lineage(self, 'sample-renewal.conf')
+ self._test_renewal_common(True, [], should_renew=True,
+ args=['renew', '--dry-run', '--cert-name', 'sample-renewal'])
+
+ def test_renew_with_bad_certname(self):
+ self._test_renewal_common(True, [], should_renew=False,
+ args=['renew', '--dry-run', '--cert-name', 'sample-renewal'],
+ error_expected=True)
+
+ def _make_dummy_renewal_config(self):
+ renewer_configs_dir = os.path.join(self.config_dir, 'renewal')
+ os.makedirs(renewer_configs_dir)
+ with open(os.path.join(renewer_configs_dir, 'test.conf'), 'w') as f:
+ f.write("My contents don't matter")
+
+ def _test_renew_common(self, renewalparams=None, names=None,
+ assert_oc_called=None, **kwargs):
+ self._make_dummy_renewal_config()
+ with mock.patch('certbot.storage.RenewableCert') as mock_rc:
+ mock_lineage = mock.MagicMock()
+ mock_lineage.fullchain = "somepath/fullchain.pem"
+ if renewalparams is not None:
+ mock_lineage.configuration = {'renewalparams': renewalparams}
+ if names is not None:
+ mock_lineage.names.return_value = names
+ mock_rc.return_value = mock_lineage
+ with mock.patch('certbot.main.obtain_cert') as mock_obtain_cert:
+ kwargs.setdefault('args', ['renew'])
+ self._test_renewal_common(True, None, should_renew=False, **kwargs)
+
+ if assert_oc_called is not None:
+ if assert_oc_called:
+ self.assertTrue(mock_obtain_cert.called)
+ else:
+ self.assertFalse(mock_obtain_cert.called)
+
+ def test_renew_no_renewalparams(self):
+ self._test_renew_common(assert_oc_called=False, error_expected=True)
+
+ def test_renew_no_authenticator(self):
+ self._test_renew_common(renewalparams={}, assert_oc_called=False,
+ error_expected=True)
+
+ def test_renew_with_bad_int(self):
+ renewalparams = {'authenticator': 'webroot',
+ 'rsa_key_size': 'over 9000'}
+ self._test_renew_common(renewalparams=renewalparams, error_expected=True,
+ assert_oc_called=False)
+
+ def test_renew_with_nonetype_http01(self):
+ renewalparams = {'authenticator': 'webroot',
+ 'http01_port': 'None'}
+ self._test_renew_common(renewalparams=renewalparams,
+ assert_oc_called=True)
+
+ def test_renew_with_bad_domain(self):
+ renewalparams = {'authenticator': 'webroot'}
+ names = ['*.example.com']
+ self._test_renew_common(renewalparams=renewalparams, error_expected=True,
+ names=names, assert_oc_called=False)
+
+ def test_renew_with_configurator(self):
+ renewalparams = {'authenticator': 'webroot'}
+ self._test_renew_common(
+ renewalparams=renewalparams, assert_oc_called=True,
+ args='renew --configurator apache'.split())
+
+ def test_renew_plugin_config_restoration(self):
+ renewalparams = {'authenticator': 'webroot',
+ 'webroot_path': 'None',
+ 'webroot_imaginary_flag': '42'}
+ self._test_renew_common(renewalparams=renewalparams,
+ assert_oc_called=True)
+
+ def test_renew_with_webroot_map(self):
+ renewalparams = {'authenticator': 'webroot'}
+ self._test_renew_common(
+ renewalparams=renewalparams, assert_oc_called=True,
+ args=['renew', '--webroot-map', '{"example.com": "/tmp"}'])
+
+ def test_renew_reconstitute_error(self):
+ # pylint: disable=protected-access
+ with mock.patch('certbot.main.renewal._reconstitute') as mock_reconstitute:
+ mock_reconstitute.side_effect = Exception
+ self._test_renew_common(assert_oc_called=False, error_expected=True)
+
+ def test_renew_obtain_cert_error(self):
+ self._make_dummy_renewal_config()
+ with mock.patch('certbot.storage.RenewableCert') as mock_rc:
+ mock_lineage = mock.MagicMock()
+ mock_lineage.fullchain = "somewhere/fullchain.pem"
+ mock_rc.return_value = mock_lineage
+ mock_lineage.configuration = {
+ 'renewalparams': {'authenticator': 'webroot'}}
+ with mock.patch('certbot.main.obtain_cert') as mock_obtain_cert:
+ mock_obtain_cert.side_effect = Exception
+ self._test_renewal_common(True, None, error_expected=True,
+ args=['renew'], should_renew=False)
+
+ def test_renew_with_bad_cli_args(self):
+ self._test_renewal_common(True, None, args='renew -d example.com'.split(),
+ should_renew=False, error_expected=True)
+ self._test_renewal_common(True, None, args='renew --csr {0}'.format(CSR).split(),
+ should_renew=False, error_expected=True)
+
+ def test_no_renewal_with_hooks(self):
+ _, _, stdout = self._test_renewal_common(
+ due_for_renewal=False, extra_args=None, should_renew=False,
+ args=['renew', '--post-hook', 'echo hello world'])
+ self.assertTrue('No hooks were run.' in stdout.getvalue())
+
+ @test_util.patch_get_utility()
+ @mock.patch('certbot.main._find_lineage_for_domains_and_certname')
+ @mock.patch('certbot.main._init_le_client')
+ def test_certonly_reinstall(self, mock_init, mock_renewal, mock_get_utility):
+ mock_renewal.return_value = ('reinstall', mock.MagicMock())
+ mock_init.return_value = mock_client = mock.MagicMock()
+ self._call(['-d', 'foo.bar', '-a', 'standalone', 'certonly'])
+ self.assertFalse(mock_client.obtain_certificate.called)
+ self.assertFalse(mock_client.obtain_and_enroll_certificate.called)
+ self.assertEqual(mock_get_utility().add_message.call_count, 0)
+ #self.assertTrue('donate' not in mock_get_utility().add_message.call_args[0][0])
+
+ def _test_certonly_csr_common(self, extra_args=None):
+ certr = 'certr'
+ chain = 'chain'
+ mock_client = mock.MagicMock()
+ mock_client.obtain_certificate_from_csr.return_value = (certr, chain)
+ cert_path = '/etc/letsencrypt/live/example.com/cert.pem'
+ mock_client.save_certificate.return_value = cert_path, None, None
+ with mock.patch('certbot.main._init_le_client') as mock_init:
+ mock_init.return_value = mock_client
+ with test_util.patch_get_utility() as mock_get_utility:
+ chain_path = '/etc/letsencrypt/live/example.com/chain.pem'
+ full_path = '/etc/letsencrypt/live/example.com/fullchain.pem'
+ args = ('-a standalone certonly --csr {0} --cert-path {1} '
+ '--chain-path {2} --fullchain-path {3}').format(
+ CSR, cert_path, chain_path, full_path).split()
+ if extra_args:
+ args += extra_args
+ with mock.patch('certbot.main.crypto_util'):
+ self._call(args)
+
+ if '--dry-run' in args:
+ self.assertFalse(mock_client.save_certificate.called)
+ else:
+ mock_client.save_certificate.assert_called_once_with(
+ certr, chain, cert_path, chain_path, full_path)
+
+ return mock_get_utility
+
+ def test_certonly_csr(self):
+ mock_get_utility = self._test_certonly_csr_common()
+ cert_msg = mock_get_utility().add_message.call_args_list[0][0][0]
+ self.assertTrue('cert.pem' in cert_msg)
+ self.assertTrue(
+ 'donate' in mock_get_utility().add_message.call_args[0][0])
+
+ def test_certonly_csr_dry_run(self):
+ mock_get_utility = self._test_certonly_csr_common(['--dry-run'])
+ self.assertEqual(mock_get_utility().add_message.call_count, 1)
+ self.assertTrue(
+ 'dry run' in mock_get_utility().add_message.call_args[0][0])
+
+ @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,
+ '--server', server, 'revoke'])
+ with open(KEY, '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:
+ 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)
+
+ @mock.patch('certbot.main._determine_account')
+ def test_revoke_without_key(self, mock_determine_account):
+ mock_determine_account.return_value = (mock.MagicMock(), None)
+ _, _, _, client = self._call(['--cert-path', CERT, 'revoke'])
+ with open(CERT) as f:
+ cert = crypto_util.pyopenssl_load_certificate(f.read())[0]
+ mock_revoke = client.acme_from_config_key().revoke
+ mock_revoke.assert_called_once_with(
+ jose.ComparableX509(cert),
+ mock.ANY)
+
+ def test_agree_dev_preview_config(self):
+ with mock.patch('certbot.main.run') as mocked_run:
+ self._call(['-c', test_util.vector_path('cli.ini')])
+ self.assertTrue(mocked_run.called)
+
+ def test_register(self):
+ with mock.patch('certbot.main.client') as mocked_client:
+ acc = mock.MagicMock()
+ acc.id = "imaginary_account"
+ mocked_client.register.return_value = (acc, "worked")
+ self._call_no_clientmock(["register", "--email", "user@example.org"])
+ # TODO: It would be more correct to explicitly check that
+ # _determine_account() gets called in the above case,
+ # but coverage statistics should also show that it did.
+ with mock.patch('certbot.main.account') as mocked_account:
+ mocked_storage = mock.MagicMock()
+ mocked_account.AccountFileStorage.return_value = mocked_storage
+ mocked_storage.find_all.return_value = ["an account"]
+ x = self._call_no_clientmock(["register", "--email", "user@example.org"])
+ self.assertTrue("There is an existing account" in x[0])
+
+ def test_update_registration_no_existing_accounts(self):
+ # with mock.patch('certbot.main.client') as mocked_client:
+ with mock.patch('certbot.main.account') as mocked_account:
+ mocked_storage = mock.MagicMock()
+ mocked_account.AccountFileStorage.return_value = mocked_storage
+ mocked_storage.find_all.return_value = []
+ x = self._call_no_clientmock(
+ ["register", "--update-registration", "--email",
+ "user@example.org"])
+ self.assertTrue("Could not find an existing account" in x[0])
+
+ def test_update_registration_unsafely(self):
+ # This test will become obsolete when register --update-registration
+ # supports removing an e-mail address from the account
+ with mock.patch('certbot.main.account') as mocked_account:
+ mocked_storage = mock.MagicMock()
+ mocked_account.AccountFileStorage.return_value = mocked_storage
+ mocked_storage.find_all.return_value = ["an account"]
+ x = self._call_no_clientmock(
+ "register --update-registration "
+ "--register-unsafely-without-email".split())
+ self.assertTrue("--register-unsafely-without-email" in x[0])
+
+ @mock.patch('certbot.main.display_ops.get_email')
+ @test_util.patch_get_utility()
+ def test_update_registration_with_email(self, mock_utility, mock_email):
+ email = "user@example.com"
+ mock_email.return_value = email
+ with mock.patch('certbot.main.client') as mocked_client:
+ with mock.patch('certbot.main.account') as mocked_account:
+ with mock.patch('certbot.main._determine_account') as mocked_det:
+ with mock.patch('certbot.main.client') as mocked_client:
+ mocked_storage = mock.MagicMock()
+ mocked_account.AccountFileStorage.return_value = mocked_storage
+ mocked_storage.find_all.return_value = ["an account"]
+ mocked_det.return_value = (mock.MagicMock(), "foo")
+ acme_client = mock.MagicMock()
+ mocked_client.Client.return_value = acme_client
+ x = self._call_no_clientmock(
+ ["register", "--update-registration"])
+ # When registration change succeeds, the return value
+ # of register() is None
+ self.assertTrue(x[0] is None)
+ # and we got supposedly did update the registration from
+ # the server
+ self.assertTrue(
+ acme_client.acme.update_registration.called)
+ # and we saved the updated registration on disk
+ self.assertTrue(mocked_storage.save_regr.called)
+ self.assertTrue(
+ email in mock_utility().add_message.call_args[0][0])
+
+
+class TestHandleException(unittest.TestCase):
+ """Test main._handle_exception"""
+ @mock.patch('certbot.main.sys')
+ def test_handle_exception(self, mock_sys):
+ # pylint: disable=protected-access
+ from acme import messages
+
+ config = mock.MagicMock()
+ mock_open = mock.mock_open()
+
+ with mock.patch('certbot.main.open', mock_open, create=True):
+ exception = Exception('detail')
+ config.verbose_count = 1
+ main._handle_exception(
+ Exception, exc_value=exception, trace=None, config=None)
+ mock_open().write.assert_any_call(''.join(
+ traceback.format_exception_only(Exception, exception)))
+ error_msg = mock_sys.exit.call_args_list[0][0][0]
+ self.assertTrue('unexpected error' in error_msg)
+
+ with mock.patch('certbot.main.open', mock_open, create=True):
+ mock_open.side_effect = [KeyboardInterrupt]
+ error = errors.Error('detail')
+ main._handle_exception(
+ errors.Error, exc_value=error, trace=None, config=None)
+ # assert_any_call used because sys.exit doesn't exit in cli.py
+ mock_sys.exit.assert_any_call(''.join(
+ traceback.format_exception_only(errors.Error, error)))
+
+ bad_typ = messages.ERROR_PREFIX + 'triffid'
+ exception = messages.Error(detail='alpha', typ=bad_typ, title='beta')
+ config = mock.MagicMock(debug=False, verbose_count=-3)
+ main._handle_exception(
+ messages.Error, exc_value=exception, trace=None, config=config)
+ error_msg = mock_sys.exit.call_args_list[-1][0][0]
+ self.assertTrue('unexpected error' in error_msg)
+ self.assertTrue('acme:error' not in error_msg)
+ self.assertTrue('alpha' in error_msg)
+ self.assertTrue('beta' in error_msg)
+ config = mock.MagicMock(debug=False, verbose_count=1)
+ main._handle_exception(
+ messages.Error, exc_value=exception, trace=None, config=config)
+ error_msg = mock_sys.exit.call_args_list[-1][0][0]
+ self.assertTrue('unexpected error' in error_msg)
+ self.assertTrue('acme:error' in error_msg)
+ self.assertTrue('alpha' in error_msg)
+
+ interrupt = KeyboardInterrupt('detail')
+ main._handle_exception(
+ KeyboardInterrupt, exc_value=interrupt, trace=None, config=None)
+ mock_sys.exit.assert_called_with(''.join(
+ traceback.format_exception_only(KeyboardInterrupt, interrupt)))
+
+
if __name__ == '__main__':
unittest.main() # pragma: no cover
diff --git a/certbot/tests/ocsp_test.py b/certbot/tests/ocsp_test.py
new file mode 100644
index 000000000..549e83ca8
--- /dev/null
+++ b/certbot/tests/ocsp_test.py
@@ -0,0 +1,170 @@
+"""Tests for ocsp.py"""
+# pylint: disable=protected-access
+
+import unittest
+
+import mock
+
+from certbot import errors
+
+out = """Missing = in header key=value
+ocsp: Use -help for summary.
+"""
+
+class OCSPTest(unittest.TestCase):
+
+ _multiprocess_can_split_ = True
+
+ def setUp(self):
+ from certbot import ocsp
+ with mock.patch('certbot.ocsp.Popen') as mock_popen:
+ with mock.patch('certbot.util.exe_exists') as mock_exists:
+ mock_communicate = mock.MagicMock()
+ mock_communicate.communicate.return_value = (None, out)
+ mock_popen.return_value = mock_communicate
+ mock_exists.return_value = True
+ self.checker = ocsp.RevocationChecker()
+
+ def tearDown(self):
+ pass
+
+ @mock.patch('certbot.ocsp.logging.info')
+ @mock.patch('certbot.ocsp.Popen')
+ @mock.patch('certbot.util.exe_exists')
+ def test_init(self, mock_exists, mock_popen, mock_log):
+ mock_communicate = mock.MagicMock()
+ mock_communicate.communicate.return_value = (None, out)
+ mock_popen.return_value = mock_communicate
+ mock_exists.return_value = True
+
+ from certbot import ocsp
+ checker = ocsp.RevocationChecker()
+ self.assertEqual(mock_popen.call_count, 1)
+ self.assertEqual(checker.host_args("x"), ["Host=x"])
+
+ mock_communicate.communicate.return_value = (None, out.partition("\n")[2])
+ checker = ocsp.RevocationChecker()
+ self.assertEqual(checker.host_args("x"), ["Host", "x"])
+ self.assertEqual(checker.broken, False)
+
+ mock_exists.return_value = False
+ mock_popen.call_count = 0
+ checker = ocsp.RevocationChecker()
+ self.assertEqual(mock_popen.call_count, 0)
+ self.assertEqual(mock_log.call_count, 1)
+ self.assertEqual(checker.broken, True)
+
+ @mock.patch('certbot.ocsp.RevocationChecker.determine_ocsp_server')
+ @mock.patch('certbot.util.run_script')
+ def test_ocsp_revoked(self, mock_run, mock_determine):
+ self.checker.broken = True
+ mock_determine.return_value = ("", "")
+ self.assertEqual(self.checker.ocsp_revoked("x", "y"), False)
+
+ self.checker.broken = False
+ mock_run.return_value = tuple(openssl_happy[1:])
+ self.assertEqual(self.checker.ocsp_revoked("x", "y"), False)
+ self.assertEqual(mock_run.call_count, 0)
+
+ mock_determine.return_value = ("http://x.co", "x.co")
+ self.assertEqual(self.checker.ocsp_revoked("blah.pem", "chain.pem"), False)
+ mock_run.side_effect = errors.SubprocessError("Unable to load certificate launcher")
+ self.assertEqual(self.checker.ocsp_revoked("x", "y"), False)
+ self.assertEqual(mock_run.call_count, 2)
+
+
+ @mock.patch('certbot.ocsp.logger.info')
+ @mock.patch('certbot.util.run_script')
+ def test_determine_ocsp_server(self, mock_run, mock_info):
+ uri = "http://ocsp.stg-int-x1.letsencrypt.org/"
+ host = "ocsp.stg-int-x1.letsencrypt.org"
+ mock_run.return_value = uri, ""
+ self.assertEqual(self.checker.determine_ocsp_server("beep"), (uri, host))
+ mock_run.return_value = "ftp:/" + host + "/", ""
+ self.assertEqual(self.checker.determine_ocsp_server("beep"), (None, None))
+ self.assertEqual(mock_info.call_count, 1)
+
+ c = "confusion"
+ mock_run.side_effect = errors.SubprocessError(c)
+ self.assertEqual(self.checker.determine_ocsp_server("beep"), (None, None))
+
+ @mock.patch('certbot.ocsp.logger')
+ @mock.patch('certbot.util.run_script')
+ def test_translate_ocsp(self, mock_run, mock_log):
+ # pylint: disable=protected-access,star-args
+ mock_run.return_value = openssl_confused
+ from certbot import ocsp
+ self.assertEqual(ocsp._translate_ocsp_query(*openssl_happy), False)
+ self.assertEqual(ocsp._translate_ocsp_query(*openssl_confused), False)
+ self.assertEqual(mock_log.debug.call_count, 1)
+ self.assertEqual(mock_log.warn.call_count, 0)
+ mock_log.debug.call_count = 0
+ self.assertEqual(ocsp._translate_ocsp_query(*openssl_unknown), False)
+ self.assertEqual(mock_log.debug.call_count, 1)
+ self.assertEqual(mock_log.warn.call_count, 0)
+ self.assertEqual(ocsp._translate_ocsp_query(*openssl_expired_ocsp), False)
+ self.assertEqual(mock_log.debug.call_count, 2)
+ self.assertEqual(ocsp._translate_ocsp_query(*openssl_broken), False)
+ self.assertEqual(mock_log.warn.call_count, 1)
+ mock_log.info.call_count = 0
+ self.assertEqual(ocsp._translate_ocsp_query(*openssl_revoked), True)
+ self.assertEqual(mock_log.info.call_count, 0)
+ self.assertEqual(ocsp._translate_ocsp_query(*openssl_expired_ocsp_revoked), True)
+ self.assertEqual(mock_log.info.call_count, 1)
+
+
+# pylint: disable=line-too-long
+openssl_confused = ("", """
+/etc/letsencrypt/live/example.org/cert.pem: good
+ This Update: Dec 17 00:00:00 2016 GMT
+ Next Update: Dec 24 00:00:00 2016 GMT
+""",
+"""
+Response Verify Failure
+139903674214048:error:27069065:OCSP routines:OCSP_basic_verify:certificate verify error:ocsp_vfy.c:138:Verify error:unable to get local issuer certificate
+""")
+
+openssl_happy = ("blah.pem", """
+blah.pem: good
+ This Update: Dec 20 18:00:00 2016 GMT
+ Next Update: Dec 27 18:00:00 2016 GMT
+""",
+"Response verify OK")
+
+openssl_revoked = ("blah.pem", """
+blah.pem: revoked
+ This Update: Dec 20 01:00:00 2016 GMT
+ Next Update: Dec 27 01:00:00 2016 GMT
+ Revocation Time: Dec 20 01:46:34 2016 GMT
+""",
+"""Response verify OK""")
+
+openssl_unknown = ("blah.pem", """
+blah.pem: unknown
+ This Update: Dec 20 18:00:00 2016 GMT
+ Next Update: Dec 27 18:00:00 2016 GMT
+""",
+"Response verify OK")
+
+openssl_broken = ("", "tentacles", "Response verify OK")
+
+openssl_expired_ocsp = ("blah.pem", """
+blah.pem: WARNING: Status times invalid.
+140659132298912:error:2707307D:OCSP routines:OCSP_check_validity:status expired:ocsp_cl.c:372:
+good
+ This Update: Apr 6 00:00:00 2016 GMT
+ Next Update: Apr 13 00:00:00 2016 GMT
+""",
+"""Response verify OK""")
+
+openssl_expired_ocsp_revoked = ("blah.pem", """
+blah.pem: WARNING: Status times invalid.
+140659132298912:error:2707307D:OCSP routines:OCSP_check_validity:status expired:ocsp_cl.c:372:
+revoked
+ This Update: Apr 6 00:00:00 2016 GMT
+ Next Update: Apr 13 00:00:00 2016 GMT
+""",
+"""Response verify OK""")
+
+if __name__ == '__main__':
+ unittest.main() # pragma: no cover
diff --git a/certbot/tests/renewal_test.py b/certbot/tests/renewal_test.py
new file mode 100644
index 000000000..07c4eac00
--- /dev/null
+++ b/certbot/tests/renewal_test.py
@@ -0,0 +1,70 @@
+"""Tests for certbot.renewal"""
+import os
+import mock
+import unittest
+import tempfile
+
+from certbot import configuration
+from certbot import errors
+from certbot import storage
+
+from certbot.tests import util
+
+
+class RenewalTest(unittest.TestCase):
+ def setUp(self):
+ self.tmp_dir = tempfile.mkdtemp()
+ self.config_dir = os.path.join(self.tmp_dir, 'config')
+
+ @mock.patch('certbot.cli.set_by_cli')
+ def test_ancient_webroot_renewal_conf(self, mock_set_by_cli):
+ mock_set_by_cli.return_value = False
+ rc_path = util.make_lineage(self, 'sample-renewal-ancient.conf')
+ args = mock.MagicMock(account=None, email=None, webroot_path=None)
+ config = configuration.NamespaceConfig(args)
+ lineage = storage.RenewableCert(rc_path, config)
+ renewalparams = lineage.configuration['renewalparams']
+ # pylint: disable=protected-access
+ from certbot import renewal
+ renewal._restore_webroot_config(config, renewalparams)
+ self.assertEqual(config.webroot_path, ['/var/www/'])
+
+
+class RestoreRequiredConfigElementsTest(unittest.TestCase):
+ """Tests for certbot.renewal.restore_required_config_elements."""
+ def setUp(self):
+ self.config = mock.MagicMock()
+
+ @classmethod
+ def _call(cls, *args, **kwargs):
+ from certbot.renewal import restore_required_config_elements
+ return restore_required_config_elements(*args, **kwargs)
+
+ @mock.patch('certbot.renewal.cli.set_by_cli')
+ def test_allow_subset_of_names_success(self, mock_set_by_cli):
+ mock_set_by_cli.return_value = False
+ self._call(self.config, {'allow_subset_of_names': 'True'})
+ self.assertTrue(self.config.namespace.allow_subset_of_names is True)
+
+ @mock.patch('certbot.renewal.cli.set_by_cli')
+ def test_allow_subset_of_names_failure(self, mock_set_by_cli):
+ mock_set_by_cli.return_value = False
+ renewalparams = {'allow_subset_of_names': 'maybe'}
+ self.assertRaises(
+ errors.Error, self._call, self.config, renewalparams)
+
+ @mock.patch('certbot.renewal.cli.set_by_cli')
+ def test_must_staple_success(self, mock_set_by_cli):
+ mock_set_by_cli.return_value = False
+ self._call(self.config, {'must_staple': 'True'})
+ self.assertTrue(self.config.namespace.must_staple is True)
+
+ @mock.patch('certbot.renewal.cli.set_by_cli')
+ def test_must_staple_failure(self, mock_set_by_cli):
+ mock_set_by_cli.return_value = False
+ renewalparams = {'must_staple': 'maybe'}
+ self.assertRaises(
+ errors.Error, self._call, self.config, renewalparams)
+
+if __name__ == "__main__":
+ unittest.main() # pragma: no cover
diff --git a/certbot/tests/reporter_test.py b/certbot/tests/reporter_test.py
index 02c7981b7..0b06cccd7 100644
--- a/certbot/tests/reporter_test.py
+++ b/certbot/tests/reporter_test.py
@@ -8,7 +8,6 @@ import six
class ReporterTest(unittest.TestCase):
"""Tests for certbot.reporter.Reporter."""
-
def setUp(self):
from certbot import reporter
self.reporter = reporter.Reporter(mock.MagicMock(quiet=False))
@@ -21,7 +20,7 @@ class ReporterTest(unittest.TestCase):
def test_multiline_message(self):
self.reporter.add_message("Line 1\nLine 2", self.reporter.LOW_PRIORITY)
- self.reporter.atexit_print_messages()
+ self.reporter.print_messages()
output = sys.stdout.getvalue()
self.assertTrue("Line 1\n" in output)
self.assertTrue("Line 2" in output)
@@ -39,9 +38,12 @@ class ReporterTest(unittest.TestCase):
self.reporter.print_messages()
self.assertEqual(sys.stdout.getvalue(), "")
- def test_atexit_print_messages(self):
+ @mock.patch('certbot.reporter.os.getpid')
+ def test_atexit_print_messages(self, mock_getpid):
self._add_messages()
- self.reporter.atexit_print_messages()
+ mock_getpid.return_value = 42
+ with mock.patch('certbot.reporter.INITIAL_PID', 42):
+ self.reporter.atexit_print_messages()
output = sys.stdout.getvalue()
self.assertTrue("IMPORTANT NOTES:" in output)
self.assertTrue("High" in output)
diff --git a/certbot/tests/reverter_test.py b/certbot/tests/reverter_test.py
index 62a43f0fe..2eeabe116 100644
--- a/certbot/tests/reverter_test.py
+++ b/certbot/tests/reverter_test.py
@@ -11,6 +11,8 @@ import six
from certbot import errors
+from certbot.tests import util as test_util
+
class ReverterCheckpointLocalTest(unittest.TestCase):
# pylint: disable=too-many-instance-attributes, too-many-public-methods
@@ -375,7 +377,7 @@ class TestFullCheckpointsReverter(unittest.TestCase):
self.assertEqual(read_in(self.config2), "directive-dir2")
self.assertFalse(os.path.isfile(config3))
- @mock.patch("certbot.reverter.zope.component.getUtility")
+ @test_util.patch_get_utility()
def test_view_config_changes(self, mock_output):
"""This is not strict as this is subject to change."""
self._setup_three_checkpoints()
diff --git a/certbot/tests/storage_test.py b/certbot/tests/storage_test.py
index 9566e0aec..f52f31d3d 100644
--- a/certbot/tests/storage_test.py
+++ b/certbot/tests/storage_test.py
@@ -17,10 +17,10 @@ from certbot import configuration
from certbot import errors
from certbot.storage import ALL_FOUR
-from certbot.tests import test_util
+from certbot.tests import util
-CERT = test_util.load_cert('cert.pem')
+CERT = util.load_cert('cert.pem')
def unlink_all(rc_object):
@@ -43,11 +43,13 @@ class BaseRenewableCertTest(unittest.TestCase):
your test. Check :class:`.cli_test.DuplicateCertTest` for an example.
"""
+ _multiprocess_can_split_ = True
+
def setUp(self):
from certbot import storage
self.tempdir = tempfile.mkdtemp()
- self.cli_config = configuration.RenewerConfiguration(
+ self.cli_config = configuration.NamespaceConfig(
namespace=mock.MagicMock(
config_dir=self.tempdir,
work_dir=self.tempdir,
@@ -55,16 +57,22 @@ class BaseRenewableCertTest(unittest.TestCase):
)
)
- # TODO: maybe provide RenewerConfiguration.make_dirs?
+ # TODO: maybe provide NamespaceConfig.make_dirs?
# TODO: main() should create those dirs, c.f. #902
os.makedirs(os.path.join(self.tempdir, "live", "example.org"))
- os.makedirs(os.path.join(self.tempdir, "archive", "example.org"))
+ archive_path = os.path.join(self.tempdir, "archive", "example.org")
+ os.makedirs(archive_path)
os.makedirs(os.path.join(self.tempdir, "renewal"))
config = configobj.ConfigObj()
for kind in ALL_FOUR:
- config[kind] = os.path.join(self.tempdir, "live", "example.org",
+ kind_path = os.path.join(self.tempdir, "live", "example.org",
kind + ".pem")
+ config[kind] = kind_path
+ with open(os.path.join(self.tempdir, "live", "example.org",
+ "README"), 'a'):
+ pass
+ config["archive"] = archive_path
config.filename = os.path.join(self.tempdir, "renewal",
"example.org.conf")
config.write()
@@ -363,18 +371,18 @@ class RenewableCertTests(BaseRenewableCertTest):
def test_names(self):
# Trying the current version
- self._write_out_kind("cert", 12, test_util.load_vector("cert-san.pem"))
+ self._write_out_kind("cert", 12, util.load_vector("cert-san.pem"))
self.assertEqual(self.test_rc.names(),
["example.com", "www.example.com"])
# Trying a non-current version
- self._write_out_kind("cert", 15, test_util.load_vector("cert.pem"))
+ self._write_out_kind("cert", 15, util.load_vector("cert.pem"))
self.assertEqual(self.test_rc.names(12),
["example.com", "www.example.com"])
# Testing common name is listed first
self._write_out_kind(
- "cert", 12, test_util.load_vector("cert-5sans.pem"))
+ "cert", 12, util.load_vector("cert-5sans.pem"))
self.assertEqual(
self.test_rc.names(12),
["example.com"] + ["{0}.example.com".format(c) for c in "abcd"])
@@ -387,7 +395,7 @@ class RenewableCertTests(BaseRenewableCertTest):
def test_time_interval_judgments(self, mock_datetime):
"""Test should_autodeploy() and should_autorenew() on the basis
of expiry time windows."""
- test_cert = test_util.load_vector("cert.pem")
+ test_cert = util.load_vector("cert.pem")
self._write_out_ex_kinds()
self.test_rc.update_all_links_to(12)
@@ -543,7 +551,8 @@ class RenewableCertTests(BaseRenewableCertTest):
from certbot.storage import relevant_values
with mock.patch("certbot.cli.helpful_parser", mock_parser):
- return relevant_values(values)
+ # make a copy to ensure values isn't modified
+ return relevant_values(values.copy())
def test_relevant_values(self):
"""Test that relevant_values() can reject an irrelevant value."""
@@ -559,10 +568,18 @@ class RenewableCertTests(BaseRenewableCertTest):
def test_relevant_values_nondefault(self):
"""Test that relevant_values() can retain a non-default value."""
values = {"rsa_key_size": 12}
- # A copy is given to _test_relevant_values_common
- # to make sure values isn't modified by the method
self.assertEqual(
- self._test_relevant_values_common(values.copy()), values)
+ self._test_relevant_values_common(values), values)
+
+ def test_relevant_values_bool(self):
+ values = {"allow_subset_of_names": True}
+ self.assertEqual(
+ self._test_relevant_values_common(values), values)
+
+ def test_relevant_values_str(self):
+ values = {"authenticator": "apache"}
+ self.assertEqual(
+ self._test_relevant_values_common(values), values)
@mock.patch("certbot.storage.relevant_values")
def test_new_lineage(self, mock_rv):
@@ -573,34 +590,38 @@ class RenewableCertTests(BaseRenewableCertTest):
from certbot import storage
result = storage.RenewableCert.new_lineage(
- "the-lineage.com", "cert", "privkey", "chain", self.cli_config)
+ "the-lineage.com", b"cert", b"privkey", b"chain", self.cli_config)
# This consistency check tests most relevant properties about the
# newly created cert lineage.
# pylint: disable=protected-access
self.assertTrue(result._consistent())
self.assertTrue(os.path.exists(os.path.join(
self.cli_config.renewal_configs_dir, "the-lineage.com.conf")))
- with open(result.fullchain) as f:
- self.assertEqual(f.read(), "cert" + "chain")
+ self.assertTrue(os.path.exists(os.path.join(
+ self.cli_config.live_dir, "the-lineage.com", "README")))
+ with open(result.fullchain, "rb") as f:
+ self.assertEqual(f.read(), b"cert" + b"chain")
# Let's do it again and make sure it makes a different lineage
result = storage.RenewableCert.new_lineage(
- "the-lineage.com", "cert2", "privkey2", "chain2", self.cli_config)
+ "the-lineage.com", b"cert2", b"privkey2", b"chain2", self.cli_config)
self.assertTrue(os.path.exists(os.path.join(
self.cli_config.renewal_configs_dir, "the-lineage.com-0001.conf")))
+ self.assertTrue(os.path.exists(os.path.join(
+ self.cli_config.live_dir, "the-lineage.com-0001", "README")))
# Now trigger the detection of already existing files
os.mkdir(os.path.join(
self.cli_config.live_dir, "the-lineage.com-0002"))
self.assertRaises(errors.CertStorageError,
storage.RenewableCert.new_lineage, "the-lineage.com",
- "cert3", "privkey3", "chain3", self.cli_config)
- os.mkdir(os.path.join(self.cli_config.archive_dir, "other-example.com"))
+ b"cert3", b"privkey3", b"chain3", self.cli_config)
+ os.mkdir(os.path.join(self.cli_config.default_archive_dir, "other-example.com"))
self.assertRaises(errors.CertStorageError,
storage.RenewableCert.new_lineage,
- "other-example.com", "cert4",
- "privkey4", "chain4", self.cli_config)
+ "other-example.com", b"cert4",
+ b"privkey4", b"chain4", self.cli_config)
# Make sure it can accept renewal parameters
result = storage.RenewableCert.new_lineage(
- "the-lineage.com", "cert2", "privkey2", "chain2", self.cli_config)
+ "the-lineage.com", b"cert2", b"privkey2", b"chain2", self.cli_config)
# TODO: Conceivably we could test that the renewal parameters actually
# got saved
@@ -613,18 +634,18 @@ class RenewableCertTests(BaseRenewableCertTest):
from certbot import storage
shutil.rmtree(self.cli_config.renewal_configs_dir)
- shutil.rmtree(self.cli_config.archive_dir)
+ shutil.rmtree(self.cli_config.default_archive_dir)
shutil.rmtree(self.cli_config.live_dir)
storage.RenewableCert.new_lineage(
- "the-lineage.com", "cert2", "privkey2", "chain2", self.cli_config)
+ "the-lineage.com", b"cert2", b"privkey2", b"chain2", self.cli_config)
self.assertTrue(os.path.exists(
os.path.join(
self.cli_config.renewal_configs_dir, "the-lineage.com.conf")))
self.assertTrue(os.path.exists(os.path.join(
self.cli_config.live_dir, "the-lineage.com", "privkey.pem")))
self.assertTrue(os.path.exists(os.path.join(
- self.cli_config.archive_dir, "the-lineage.com", "privkey1.pem")))
+ self.cli_config.default_archive_dir, "the-lineage.com", "privkey1.pem")))
@mock.patch("certbot.storage.util.unique_lineage_name")
def test_invalid_config_filename(self, mock_uln):
@@ -699,6 +720,19 @@ class RenewableCertTests(BaseRenewableCertTest):
self.assertEqual(storage.add_time_interval(base_time, interval),
excepted)
+ def test_is_test_cert(self):
+ self.test_rc.configuration["renewalparams"] = {}
+ rp = self.test_rc.configuration["renewalparams"]
+ self.assertEqual(self.test_rc.is_test_cert, False)
+ rp["server"] = "https://acme-staging.api.letsencrypt.org/directory"
+ self.assertEqual(self.test_rc.is_test_cert, True)
+ rp["server"] = "https://staging.someotherca.com/directory"
+ self.assertEqual(self.test_rc.is_test_cert, True)
+ rp["server"] = "https://acme-v01.api.letsencrypt.org/directory"
+ self.assertEqual(self.test_rc.is_test_cert, False)
+ rp["server"] = "https://acme-v02.api.letsencrypt.org/directory"
+ self.assertEqual(self.test_rc.is_test_cert, False)
+
def test_missing_cert(self):
from certbot import storage
self.assertRaises(errors.CertStorageError,
@@ -721,9 +755,10 @@ class RenewableCertTests(BaseRenewableCertTest):
target = {}
for x in ALL_FOUR:
target[x] = "somewhere"
+ archive_dir = "the_archive"
relevant_data = {"useful": "new_value"}
from certbot import storage
- storage.write_renewal_config(temp, temp2, target, relevant_data)
+ storage.write_renewal_config(temp, temp2, archive_dir, target, relevant_data)
with open(temp2, "r") as f:
content = f.read()
# useful value was updated
@@ -735,6 +770,110 @@ class RenewableCertTests(BaseRenewableCertTest):
# check version was stored
self.assertTrue("version = {0}".format(certbot.__version__) in content)
+ def test_update_symlinks(self):
+ from certbot import storage
+ archive_dir_path = os.path.join(self.tempdir, "archive", "example.org")
+ for kind in ALL_FOUR:
+ live_path = self.config[kind]
+ basename = kind + "1.pem"
+ archive_path = os.path.join(archive_dir_path, basename)
+ open(archive_path, 'a').close()
+ os.symlink(os.path.join(self.tempdir, basename), live_path)
+ self.assertRaises(errors.CertStorageError,
+ storage.RenewableCert, self.config.filename,
+ self.cli_config)
+ storage.RenewableCert(self.config.filename, self.cli_config,
+ update_symlinks=True)
+
+class DeleteFilesTest(BaseRenewableCertTest):
+ """Tests for certbot.storage.delete_files"""
+ def setUp(self):
+ super(DeleteFilesTest, self).setUp()
+ for kind in ALL_FOUR:
+ kind_path = os.path.join(self.tempdir, "live", "example.org",
+ kind + ".pem")
+ with open(kind_path, 'a'):
+ pass
+ self.config.write()
+ self.assertTrue(os.path.exists(os.path.join(
+ self.cli_config.renewal_configs_dir, "example.org.conf")))
+ self.assertTrue(os.path.exists(os.path.join(
+ self.cli_config.live_dir, "example.org")))
+ self.assertTrue(os.path.exists(os.path.join(
+ self.tempdir, "archive", "example.org")))
+
+ def _call(self):
+ from certbot import storage
+ with mock.patch("certbot.storage.logger"):
+ storage.delete_files(self.cli_config, "example.org")
+
+ def test_delete_all_files(self):
+ self._call()
+
+ self.assertFalse(os.path.exists(os.path.join(
+ self.cli_config.renewal_configs_dir, "example.org.conf")))
+ self.assertFalse(os.path.exists(os.path.join(
+ self.cli_config.live_dir, "example.org")))
+ self.assertFalse(os.path.exists(os.path.join(
+ self.tempdir, "archive", "example.org")))
+
+ def test_bad_renewal_config(self):
+ with open(self.config.filename, 'a') as config_file:
+ config_file.write("asdfasfasdfasdf")
+
+ self.assertRaises(errors.CertStorageError, self._call)
+ self.assertTrue(os.path.exists(os.path.join(
+ self.cli_config.live_dir, "example.org")))
+ self.assertFalse(os.path.exists(os.path.join(
+ self.cli_config.renewal_configs_dir, "example.org.conf")))
+
+ def test_no_renewal_config(self):
+ os.remove(self.config.filename)
+ self.assertRaises(errors.CertStorageError, self._call)
+ self.assertTrue(os.path.exists(os.path.join(
+ self.cli_config.live_dir, "example.org")))
+ self.assertFalse(os.path.exists(self.config.filename))
+
+ def test_no_cert_file(self):
+ os.remove(os.path.join(
+ self.cli_config.live_dir, "example.org", "cert.pem"))
+ self._call()
+ self.assertFalse(os.path.exists(self.config.filename))
+ self.assertFalse(os.path.exists(os.path.join(
+ self.cli_config.live_dir, "example.org")))
+ self.assertFalse(os.path.exists(os.path.join(
+ self.tempdir, "archive", "example.org")))
+
+ def test_no_readme_file(self):
+ os.remove(os.path.join(
+ self.cli_config.live_dir, "example.org", "README"))
+ self._call()
+ self.assertFalse(os.path.exists(self.config.filename))
+ self.assertFalse(os.path.exists(os.path.join(
+ self.cli_config.live_dir, "example.org")))
+ self.assertFalse(os.path.exists(os.path.join(
+ self.tempdir, "archive", "example.org")))
+
+ def test_livedir_not_empty(self):
+ with open(os.path.join(
+ self.cli_config.live_dir, "example.org", "other_file"), 'a'):
+ pass
+ self._call()
+ self.assertFalse(os.path.exists(self.config.filename))
+ self.assertTrue(os.path.exists(os.path.join(
+ self.cli_config.live_dir, "example.org")))
+ self.assertFalse(os.path.exists(os.path.join(
+ self.tempdir, "archive", "example.org")))
+
+ def test_no_archive(self):
+ archive_dir = os.path.join(self.tempdir, "archive", "example.org")
+ os.rmdir(archive_dir)
+ self._call()
+ self.assertFalse(os.path.exists(self.config.filename))
+ self.assertFalse(os.path.exists(os.path.join(
+ self.cli_config.live_dir, "example.org")))
+ self.assertFalse(os.path.exists(archive_dir))
+
if __name__ == "__main__":
unittest.main() # pragma: no cover
diff --git a/certbot/tests/test_util.py b/certbot/tests/test_util.py
deleted file mode 100644
index ba968511f..000000000
--- a/certbot/tests/test_util.py
+++ /dev/null
@@ -1,113 +0,0 @@
-"""Test utilities.
-
-.. warning:: This module is not part of the public API.
-
-"""
-import os
-import pkg_resources
-import unittest
-
-from cryptography.hazmat.backends import default_backend
-from cryptography.hazmat.primitives import serialization
-import OpenSSL
-
-from acme import errors
-from acme import jose
-from acme import util
-
-
-def vector_path(*names):
- """Path to a test vector."""
- return pkg_resources.resource_filename(
- __name__, os.path.join('testdata', *names))
-
-
-def load_vector(*names):
- """Load contents of a test vector."""
- # luckily, resource_string opens file in binary mode
- return pkg_resources.resource_string(
- __name__, os.path.join('testdata', *names))
-
-
-def _guess_loader(filename, loader_pem, loader_der):
- _, ext = os.path.splitext(filename)
- if ext.lower() == '.pem':
- return loader_pem
- elif ext.lower() == '.der':
- return loader_der
- else: # pragma: no cover
- raise ValueError("Loader could not be recognized based on extension")
-
-
-def load_cert(*names):
- """Load certificate."""
- loader = _guess_loader(
- names[-1], OpenSSL.crypto.FILETYPE_PEM, OpenSSL.crypto.FILETYPE_ASN1)
- return OpenSSL.crypto.load_certificate(loader, load_vector(*names))
-
-
-def load_comparable_cert(*names):
- """Load ComparableX509 cert."""
- return jose.ComparableX509(load_cert(*names))
-
-
-def load_csr(*names):
- """Load certificate request."""
- loader = _guess_loader(
- names[-1], OpenSSL.crypto.FILETYPE_PEM, OpenSSL.crypto.FILETYPE_ASN1)
- return OpenSSL.crypto.load_certificate_request(loader, load_vector(*names))
-
-
-def load_comparable_csr(*names):
- """Load ComparableX509 certificate request."""
- return jose.ComparableX509(load_csr(*names))
-
-
-def load_rsa_private_key(*names):
- """Load RSA private key."""
- loader = _guess_loader(names[-1], serialization.load_pem_private_key,
- serialization.load_der_private_key)
- return jose.ComparableRSAKey(loader(
- load_vector(*names), password=None, backend=default_backend()))
-
-
-def load_pyopenssl_private_key(*names):
- """Load pyOpenSSL private key."""
- loader = _guess_loader(
- names[-1], OpenSSL.crypto.FILETYPE_PEM, OpenSSL.crypto.FILETYPE_ASN1)
- return OpenSSL.crypto.load_privatekey(loader, load_vector(*names))
-
-
-def requirement_available(requirement):
- """Checks if requirement can be imported.
-
- :rtype: bool
- :returns: ``True`` iff requirement can be imported
-
- """
- try:
- util.activate(requirement)
- except errors.DependencyError: # pragma: no cover
- return False
- return True # pragma: no cover
-
-
-def skip_unless(condition, reason): # pragma: no cover
- """Skip tests unless a condition holds.
-
- This implements the basic functionality of unittest.skipUnless
- which is only available on Python 2.7+.
-
- :param bool condition: If ``False``, the test will be skipped
- :param str reason: the reason for skipping the test
-
- :rtype: callable
- :returns: decorator that hides tests unless condition is ``True``
-
- """
- if hasattr(unittest, "skipUnless"):
- return unittest.skipUnless(condition, reason)
- elif condition:
- return lambda cls: cls
- else:
- return lambda cls: None
diff --git a/certbot/tests/testdata/sample-renewal.conf b/certbot/tests/testdata/sample-renewal.conf
index 08032af86..52b3ec45c 100644
--- a/certbot/tests/testdata/sample-renewal.conf
+++ b/certbot/tests/testdata/sample-renewal.conf
@@ -73,4 +73,5 @@ tls_sni_01_port = 443
logs_dir = /var/log/letsencrypt
apache_vhost_root = /etc/apache2/sites-available
configurator = None
+must_staple = True
[[webroot_map]]
diff --git a/certbot/tests/util.py b/certbot/tests/util.py
new file mode 100644
index 000000000..7d674e171
--- /dev/null
+++ b/certbot/tests/util.py
@@ -0,0 +1,248 @@
+"""Test utilities.
+
+.. warning:: This module is not part of the public API.
+
+"""
+import os
+import pkg_resources
+import shutil
+import unittest
+
+from cryptography.hazmat.backends import default_backend
+from cryptography.hazmat.primitives import serialization
+import mock
+import OpenSSL
+
+from acme import errors
+from acme import jose
+from acme import util
+
+from certbot import constants
+from certbot import interfaces
+from certbot import storage
+
+from certbot.display import util as display_util
+
+
+def vector_path(*names):
+ """Path to a test vector."""
+ return pkg_resources.resource_filename(
+ __name__, os.path.join('testdata', *names))
+
+
+def load_vector(*names):
+ """Load contents of a test vector."""
+ # luckily, resource_string opens file in binary mode
+ return pkg_resources.resource_string(
+ __name__, os.path.join('testdata', *names))
+
+
+def _guess_loader(filename, loader_pem, loader_der):
+ _, ext = os.path.splitext(filename)
+ if ext.lower() == '.pem':
+ return loader_pem
+ elif ext.lower() == '.der':
+ return loader_der
+ else: # pragma: no cover
+ raise ValueError("Loader could not be recognized based on extension")
+
+
+def load_cert(*names):
+ """Load certificate."""
+ loader = _guess_loader(
+ names[-1], OpenSSL.crypto.FILETYPE_PEM, OpenSSL.crypto.FILETYPE_ASN1)
+ return OpenSSL.crypto.load_certificate(loader, load_vector(*names))
+
+
+def load_comparable_cert(*names):
+ """Load ComparableX509 cert."""
+ return jose.ComparableX509(load_cert(*names))
+
+
+def load_csr(*names):
+ """Load certificate request."""
+ loader = _guess_loader(
+ names[-1], OpenSSL.crypto.FILETYPE_PEM, OpenSSL.crypto.FILETYPE_ASN1)
+ return OpenSSL.crypto.load_certificate_request(loader, load_vector(*names))
+
+
+def load_comparable_csr(*names):
+ """Load ComparableX509 certificate request."""
+ return jose.ComparableX509(load_csr(*names))
+
+
+def load_rsa_private_key(*names):
+ """Load RSA private key."""
+ loader = _guess_loader(names[-1], serialization.load_pem_private_key,
+ serialization.load_der_private_key)
+ return jose.ComparableRSAKey(loader(
+ load_vector(*names), password=None, backend=default_backend()))
+
+
+def load_pyopenssl_private_key(*names):
+ """Load pyOpenSSL private key."""
+ loader = _guess_loader(
+ names[-1], OpenSSL.crypto.FILETYPE_PEM, OpenSSL.crypto.FILETYPE_ASN1)
+ return OpenSSL.crypto.load_privatekey(loader, load_vector(*names))
+
+
+def requirement_available(requirement):
+ """Checks if requirement can be imported.
+
+ :rtype: bool
+ :returns: ``True`` iff requirement can be imported
+
+ """
+ try:
+ util.activate(requirement)
+ except errors.DependencyError: # pragma: no cover
+ return False
+ return True # pragma: no cover
+
+
+def skip_unless(condition, reason): # pragma: no cover
+ """Skip tests unless a condition holds.
+
+ This implements the basic functionality of unittest.skipUnless
+ which is only available on Python 2.7+.
+
+ :param bool condition: If ``False``, the test will be skipped
+ :param str reason: the reason for skipping the test
+
+ :rtype: callable
+ :returns: decorator that hides tests unless condition is ``True``
+
+ """
+ if hasattr(unittest, "skipUnless"):
+ return unittest.skipUnless(condition, reason)
+ elif condition:
+ return lambda cls: cls
+ else:
+ return lambda cls: None
+
+
+def make_lineage(self, testfile):
+ """Creates a lineage defined by testfile.
+
+ This creates the archive, live, and renewal directories if
+ necessary and creates a simple lineage.
+
+ :param str testfile: configuration file to base the lineage on
+
+ :returns: path to the renewal conf file for the created lineage
+ :rtype: str
+
+ """
+ lineage_name = testfile[:-len('.conf')]
+
+ conf_dir = os.path.join(
+ self.config_dir, constants.RENEWAL_CONFIGS_DIR)
+ archive_dir = os.path.join(
+ self.config_dir, constants.ARCHIVE_DIR, lineage_name)
+ live_dir = os.path.join(
+ self.config_dir, constants.LIVE_DIR, lineage_name)
+
+ for directory in (archive_dir, conf_dir, live_dir,):
+ if not os.path.exists(directory):
+ os.makedirs(directory)
+
+ sample_archive = vector_path('sample-archive')
+ for kind in os.listdir(sample_archive):
+ shutil.copyfile(os.path.join(sample_archive, kind),
+ os.path.join(archive_dir, kind))
+
+ for kind in storage.ALL_FOUR:
+ os.symlink(os.path.join(archive_dir, '{0}1.pem'.format(kind)),
+ os.path.join(live_dir, '{0}.pem'.format(kind)))
+
+ conf_path = os.path.join(self.config_dir, conf_dir, testfile)
+ with open(vector_path(testfile)) as src:
+ with open(conf_path, 'w') as dst:
+ dst.writelines(
+ line.replace('MAGICDIR', self.config_dir) for line in src)
+
+ return conf_path
+
+
+def patch_get_utility(target='zope.component.getUtility'):
+ """Patch zope.component.getUtility to use a special mock IDisplay.
+
+ The mock IDisplay works like a regular mock object, except it also
+ also asserts that methods are called with valid arguments.
+
+ :param str target: path to patch
+
+ :returns: mock zope.component.getUtility
+ :rtype: mock.MagicMock
+
+ """
+ return mock.patch(target, new_callable=_create_get_utility_mock)
+
+
+class FreezableMock(object):
+ """Mock object with the ability to freeze attributes.
+
+ This class works like a regular mock.MagicMock object, except
+ attributes and behavior can be set and frozen so they cannot be
+ changed during tests.
+
+ If a func argument is provided to the constructor, this function
+ is called first when an instance of FreezableMock is called,
+ followed by the usual behavior defined by MagicMock. The return
+ value of func is ignored.
+
+ """
+ def __init__(self, frozen=False, func=None):
+ self._frozen_set = set() if frozen else set(('freeze',))
+ self._func = func
+ self._mock = mock.MagicMock()
+ self._frozen = frozen
+
+ def freeze(self):
+ """Freeze object preventing further changes."""
+ self._frozen = True
+
+ def __call__(self, *args, **kwargs):
+ if self._func is not None:
+ self._func(*args, **kwargs)
+ return self._mock(*args, **kwargs)
+
+ def __getattribute__(self, name):
+ if name == '_frozen':
+ try:
+ return object.__getattribute__(self, name)
+ except AttributeError:
+ return False
+ elif name == '_frozen_set' or name in self._frozen_set:
+ return object.__getattribute__(self, name)
+ else:
+ return getattr(object.__getattribute__(self, '_mock'), name)
+
+ def __setattr__(self, name, value):
+ if self._frozen:
+ return setattr(self._mock, name, value)
+ elif name != '_frozen_set':
+ self._frozen_set.add(name)
+ return object.__setattr__(self, name, value)
+
+
+def _create_get_utility_mock():
+ display = FreezableMock()
+ for name in interfaces.IDisplay.names(): # pylint: disable=no-member
+ if name != 'notification':
+ frozen_mock = FreezableMock(frozen=True, func=_assert_valid_call)
+ setattr(display, name, frozen_mock)
+ display.freeze()
+ return mock.MagicMock(return_value=display)
+
+
+def _assert_valid_call(*args, **kwargs):
+ assert_args = [args[0] if args else kwargs['message']]
+
+ assert_kwargs = {}
+ assert_kwargs['default'] = kwargs.get('default', None)
+ assert_kwargs['cli_flag'] = kwargs.get('cli_flag', None)
+ assert_kwargs['force_interactive'] = kwargs.get('force_interactive', False)
+
+ # pylint: disable=star-args
+ display_util.assert_valid_call(*assert_args, **assert_kwargs)
diff --git a/certbot/tests/util_test.py b/certbot/tests/util_test.py
index 6f06c8306..6dc839025 100644
--- a/certbot/tests/util_test.py
+++ b/certbot/tests/util_test.py
@@ -11,7 +11,7 @@ import mock
import six
from certbot import errors
-from certbot.tests import test_util
+import certbot.tests.util as test_util
class RunScriptTest(unittest.TestCase):
@@ -195,6 +195,7 @@ except NameError:
import io
file_type = io.TextIOWrapper
+
class UniqueLineageNameTest(unittest.TestCase):
"""Tests for certbot.util.unique_lineage_name."""
@@ -373,6 +374,49 @@ class EnforceDomainSanityTest(unittest.TestCase):
self.assertRaises(errors.ConfigurationError, self._call,
u"eichh\u00f6rnchen.example.com")
+ def test_too_long(self):
+ long_domain = u"a"*256
+ self.assertRaises(errors.ConfigurationError, self._call,
+ long_domain)
+
+ def test_not_too_long(self):
+ not_too_long_domain = u"{0}.{1}.{2}.{3}".format("a"*63, "b"*63, "c"*63, "d"*63)
+ self._call(not_too_long_domain)
+
+ def test_empty_label(self):
+ empty_label_domain = u"fizz..example.com"
+ self.assertRaises(errors.ConfigurationError, self._call,
+ empty_label_domain)
+
+ def test_empty_trailing_label(self):
+ empty_trailing_label_domain = u"example.com.."
+ self.assertRaises(errors.ConfigurationError, self._call,
+ empty_trailing_label_domain)
+
+ def test_long_label_1(self):
+ long_label_domain = u"a"*64
+ self.assertRaises(errors.ConfigurationError, self._call,
+ long_label_domain)
+
+ def test_long_label_2(self):
+ long_label_domain = u"{0}.{1}.com".format(u"a"*64, u"b"*63)
+ self.assertRaises(errors.ConfigurationError, self._call,
+ long_label_domain)
+
+ def test_not_long_label(self):
+ not_too_long_label_domain = u"{0}.{1}.com".format(u"a"*63, u"b"*63)
+ self._call(not_too_long_label_domain)
+
+ def test_empty_domain(self):
+ empty_domain = u""
+ self.assertRaises(errors.ConfigurationError, self._call,
+ empty_domain)
+
+ def test_punycode_ok(self):
+ # Punycode is now legal, so no longer an error; instead check
+ # that it's _not_ an error (at the initial sanity check stage)
+ self._call('this.is.xn--ls8h.tld')
+
class OsInfoTest(unittest.TestCase):
"""Test OS / distribution detection"""
diff --git a/certbot/util.py b/certbot/util.py
index 577180b00..e8532fc6d 100644
--- a/certbot/util.py
+++ b/certbot/util.py
@@ -17,6 +17,7 @@ import sys
import configargparse
+from certbot import constants
from certbot import errors
@@ -37,20 +38,22 @@ ANSI_SGR_RED = "\033[31m"
ANSI_SGR_RESET = "\033[0m"
-def run_script(params):
+def run_script(params, log=logger.error):
"""Run the script with the given params.
:param list params: List of parameters to pass to Popen
+ :param logging.Logger log: Logger to use for errors
"""
try:
proc = subprocess.Popen(params,
stdout=subprocess.PIPE,
- stderr=subprocess.PIPE)
+ stderr=subprocess.PIPE,
+ universal_newlines=True)
except (OSError, ValueError):
msg = "Unable to run the command: %s" % " ".join(params)
- logger.error(msg)
+ log(msg)
raise errors.SubprocessError(msg)
stdout, stderr = proc.communicate()
@@ -59,7 +62,7 @@ def run_script(params):
msg = "Error while running %s.\n%s\n%s" % (
" ".join(params), stdout, stderr)
# Enter recovery routine...
- logger.error(msg)
+ log(msg)
raise errors.SubprocessError(msg)
return stdout, stderr
@@ -151,11 +154,11 @@ def safe_open(path, mode="w", chmod=None, buffering=None):
mode, *fdopen_args)
-def _unique_file(path, filename_pat, count, mode):
+def _unique_file(path, filename_pat, count, chmod, mode):
while True:
current_path = os.path.join(path, filename_pat(count))
try:
- return safe_open(current_path, chmod=mode),\
+ return safe_open(current_path, chmod=chmod, mode=mode),\
os.path.abspath(current_path)
except OSError as err:
# "File exists," is okay, try a different name.
@@ -164,11 +167,12 @@ def _unique_file(path, filename_pat, count, mode):
count += 1
-def unique_file(path, mode=0o777):
+def unique_file(path, chmod=0o777, mode="w"):
"""Safely finds a unique file.
:param str path: path/filename.ext
- :param int mode: File mode
+ :param int chmod: File mode
+ :param str mode: Open mode
:returns: tuple of file object and file name
@@ -176,15 +180,16 @@ def unique_file(path, mode=0o777):
path, tail = os.path.split(path)
return _unique_file(
path, filename_pat=(lambda count: "%04d_%s" % (count, tail)),
- count=0, mode=mode)
+ count=0, chmod=chmod, mode=mode)
-def unique_lineage_name(path, filename, mode=0o777):
+def unique_lineage_name(path, filename, chmod=0o644, mode="w"):
"""Safely finds a unique file using lineage convention.
:param str path: directory path
:param str filename: proposed filename
- :param int mode: file mode
+ :param int chmod: file mode
+ :param str mode: open mode
:returns: tuple of file object and file name (which may be modified
from the requested one by appending digits to ensure uniqueness)
@@ -196,13 +201,13 @@ def unique_lineage_name(path, filename, mode=0o777):
"""
preferred_path = os.path.join(path, "%s.conf" % (filename))
try:
- return safe_open(preferred_path, chmod=mode), preferred_path
+ return safe_open(preferred_path, chmod=chmod), preferred_path
except OSError as err:
if err.errno != errno.EEXIST:
raise
return _unique_file(
path, filename_pat=(lambda count: "%s-%04d.conf" % (filename, count)),
- count=1, mode=mode)
+ count=1, chmod=chmod, mode=mode)
def safely_remove(path):
@@ -422,7 +427,6 @@ def enforce_le_validity(domain):
label, domain))
return domain
-
def enforce_domain_sanity(domain):
"""Method which validates domain value and errors out if
the requirements are not met.
@@ -451,12 +455,8 @@ def enforce_domain_sanity(domain):
domain = domain.decode('utf-8')
domain.encode('ascii')
except UnicodeError:
- error_fmt = (u"Internationalized domain names "
- "are not presently supported: {0}")
- if isinstance(domain, six.text_type):
- raise errors.ConfigurationError(error_fmt.format(domain))
- else:
- raise errors.ConfigurationError(str(error_fmt).format(domain))
+ raise errors.ConfigurationError("Non-ASCII domain names not supported. "
+ "To issue for an Internationalized Domain Name, use Punycode.")
domain = domain.lower()
@@ -478,13 +478,15 @@ def enforce_domain_sanity(domain):
# FQDN checks according to RFC 2181: domain name should be less than 255
# octets (inclusive). And each label is 1 - 63 octets (inclusive).
# https://tools.ietf.org/html/rfc2181#section-11
- msg = "Requested domain {0} is not a FQDN because ".format(domain)
+ msg = "Requested domain {0} is not a FQDN because".format(domain)
+ if len(domain) > 255:
+ raise errors.ConfigurationError("{0} it is too long.".format(msg))
labels = domain.split('.')
for l in labels:
- if not 0 < len(l) < 64:
- raise errors.ConfigurationError(msg + "label {0} is too long.".format(l))
- if len(domain) > 255:
- raise errors.ConfigurationError(msg + "it is too long.")
+ if not l:
+ raise errors.ConfigurationError("{0} it contains an empty label.".format(msg))
+ elif len(l) > 63:
+ raise errors.ConfigurationError("{0} label {1} is too long.".format(msg, l))
return domain
@@ -501,3 +503,14 @@ def get_strict_version(normalized):
# strict version ending with "a" and a number designates a pre-release
# pylint: disable=no-member
return distutils.version.StrictVersion(normalized.replace(".dev", "a"))
+
+
+def is_staging(srv):
+ """
+ Determine whether a given ACME server is a known test / staging server.
+
+ :param str srv: the URI for the ACME server
+ :returns: True iff srv is a known test / staging server
+ :rtype bool:
+ """
+ return srv == constants.STAGING_URI or "staging" in srv
diff --git a/docs/api/log.rst b/docs/api/log.rst
deleted file mode 100644
index 41311de90..000000000
--- a/docs/api/log.rst
+++ /dev/null
@@ -1,5 +0,0 @@
-:mod:`certbot.log`
-----------------------
-
-.. automodule:: certbot.log
- :members:
diff --git a/docs/cli-help.txt b/docs/cli-help.txt
index f7340c48b..a2dd61a31 100644
--- a/docs/cli-help.txt
+++ b/docs/cli-help.txt
@@ -1,39 +1,61 @@
usage:
- certbot [SUBCOMMAND] [options] [-d domain] [-d domain] ...
+ certbot [SUBCOMMAND] [options] [-d DOMAIN] [-d DOMAIN] ...
Certbot can obtain and install HTTPS/TLS/SSL certificates. By default,
it will attempt to use a webserver both for obtaining and installing the
-cert. Major SUBCOMMANDS are:
+cert. The most common SUBCOMMANDS and flags are:
- (default) run Obtain & install a cert in your current webserver
- certonly Obtain cert, but do not install it (aka "auth")
- install Install a previously obtained cert in a server
- renew Renew previously obtained certs that are near expiry
- revoke Revoke a previously obtained certificate
- register Perform tasks related to registering with the CA
- rollback Rollback server configuration changes made during install
- config_changes Show changes made to server config during installation
- plugins Display information about installed plugins
+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
+
+ --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
+
+ -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
+
+manage certificates:
+ certificates Display information about certs you have from Certbot
+ revoke Revoke a certificate (supply --cert-path)
+ delete Delete a certificate
+
+manage your account with Let's Encrypt:
+ register Create a Let's Encrypt ACME account
+ --agree-tos Agree to the ACME server's Subscriber Agreement
+ -m EMAIL Email address for important account notifications
optional arguments:
-h, --help show this help message and exit
-c CONFIG_FILE, --config CONFIG_FILE
- config file path (default: None)
+ path to config file (default: /etc/letsencrypt/cli.ini
+ and ~/.config/letsencrypt/cli.ini)
-v, --verbose This flag can be used multiple times to incrementally
increase the verbosity of output, e.g. -vvv. (default:
-2)
- -t, --text Use the text output instead of the curses UI.
- (default: False)
-n, --non-interactive, --noninteractive
Run without ever asking for user input. This may
require additional command line flags; the client will
try to explain which ones are required if it finds one
missing (default: False)
- --dialog Run using interactive dialog menus (default: False)
+ --force-interactive Force Certbot to be interactive even if it detects
+ it's not being run in a terminal. This flag cannot be
+ used with the renew subcommand. (default: False)
-d DOMAIN, --domains DOMAIN, --domain DOMAIN
Domain names to apply. For multiple domains you can
use multiple -d flags or enter a comma separated list
- of domains as a parameter. (default: [])
+ of domains as a parameter. (default: Ask)
+ --cert-name CERTNAME Certificate name to apply. Only one certificate name
+ can be used per Certbot run. To see certificate names,
+ run 'certbot certificates'. When creating a new
+ 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'
@@ -48,24 +70,6 @@ optional arguments:
because they may be necessary to accurately simulate
renewal. --renew-hook commands are not called.
(default: False)
- --register-unsafely-without-email
- Specifying this flag enables registering an account
- with no email address. This is strongly discouraged,
- because in the event of key loss or account compromise
- you will irrevocably lose access to your account. You
- will also be unable to receive notice about impending
- expiration or revocation of your certificates. Updates
- to the Subscriber Agreement will still affect you, and
- will be effective 14 days after posting an update to
- the web site. (default: False)
- --update-registration
- With the register verb, indicates that details
- associated with an existing registration, such as the
- e-mail address, should be updated, rather than
- registering a new account. (default: False)
- -m EMAIL, --email EMAIL
- Email used for registration and recovery contact.
- (default: None)
--preferred-challenges PREF_CHALLS
A sorted, comma delimited list of the preferred
challenge to use during authorization with the most
@@ -81,7 +85,9 @@ optional arguments:
agent strings allow the CA to collect high level
statistics about success rates by OS and plugin. If
you wish to hide your server OS version from the Let's
- Encrypt server, set this to "". (default: None)
+ Encrypt server, set this to "". (default:
+ CertbotACMEClient/0.10.0 (Ubuntu 16.04.1 LTS)
+ Authenticator/XXX Installer/YYY)
automation:
Arguments for automating execution & other tweaks
@@ -90,16 +96,21 @@ automation:
If the requested cert matches an existing cert, always
keep the existing one until it is due for renewal (for
the 'run' subcommand this means reinstall the existing
- cert) (default: False)
+ cert). (default: Ask)
--expand If an existing cert covers some subset of the
requested names, always expand and replace it with the
- additional names. (default: False)
+ additional names. (default: Ask)
--version show program's version number and exit
--force-renewal, --renew-by-default
If a certificate already exists for the requested
domains, renew it now, regardless of whether it is
near expiry. (Often --keep-until-expiring is more
appropriate). Also implies --expand. (default: False)
+ --renew-with-new-domains
+ If a certificate already exists for the requested
+ certificate name but does not match the requested
+ domains, renew it now, regardless of whether it is
+ near expiry. (default: False)
--allow-subset-of-names
When performing domain validation, do not consider it
a failure if authorizations can not be obtained for a
@@ -108,8 +119,7 @@ automation:
succeed even if some domains no longer point at this
system. This option cannot be used with --csr.
(default: False)
- --agree-tos Agree to the ACME Subscriber Agreement (default:
- False)
+ --agree-tos Agree to the ACME Subscriber Agreement (default: Ask)
--account ACCOUNT_ID Account ID to use (default: None)
--duplicate Allow making a certificate lineage that duplicates an
existing one (both can be renewed in parallel)
@@ -118,7 +128,7 @@ automation:
and then stop (default: False)
--no-self-upgrade (certbot-auto only) prevent the certbot-auto script
from upgrading itself to newer released versions
- (default: False)
+ (default: Upgrade automatically)
-q, --quiet Silence all output except errors. Useful for
automation via cron. Implies --non-interactive.
(default: False)
@@ -132,48 +142,90 @@ security:
supported setups (Apache version >= 2.3.3 ). (default:
False)
--redirect Automatically redirect all HTTP traffic to HTTPS for
- the newly authenticated vhost. (default: None)
+ the newly authenticated vhost. (default: Ask)
--no-redirect Do not automatically redirect all HTTP traffic to
HTTPS for the newly authenticated vhost. (default:
- None)
+ Ask)
--hsts Add the Strict-Transport-Security header to every HTTP
response. Forcing browser to always use SSL for the
domain. Defends against SSL Stripping. (default:
False)
- --no-hsts Do not automatically add the Strict-Transport-Security
- header to every HTTP response. (default: False)
--uir Add the "Content-Security-Policy: upgrade-insecure-
requests" header to every HTTP response. Forcing the
browser to use https:// for every http:// resource.
(default: None)
- --no-uir Do not automatically set the "Content-Security-Policy:
- upgrade-insecure-requests" header to every HTTP
- response. (default: None)
--staple-ocsp Enables OCSP Stapling. A valid OCSP response is
stapled to the certificate that the server offers
during TLS. (default: None)
- --no-staple-ocsp Do not automatically enable OCSP Stapling. (default:
- None)
--strict-permissions Require that all configuration files are owned by the
current user; only needed if your config is somewhere
unsafe like /tmp/ (default: False)
testing:
- The following flags are meant for testing purposes only! Do NOT change
- them, unless you really know what you're doing!
+ The following flags are meant for testing and integration purposes only.
--test-cert, --staging
- Use the staging server to obtain test (invalid) certs;
- equivalent to --server https://acme-
+ Use the staging server to obtain or revoke test
+ (invalid) certs; 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)
--no-verify-ssl Disable verification of the ACME server's certificate.
(default: False)
+ --tls-sni-01-port TLS_SNI_01_PORT
+ Port used during tls-sni-01 challenge. This only
+ affects the port Certbot listens on. A conforming ACME
+ server will still attempt to connect on port 443.
+ (default: 443)
+ --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)
+paths:
+ Arguments changing execution paths & servers
+
+ --cert-path CERT_PATH
+ Path to where cert is saved (with auth --csr),
+ installed from, or revoked. (default: None)
+ --key-path KEY_PATH Path to private key for cert installation or
+ revocation (if account key is missing) (default: None)
+ --chain-path CHAIN_PATH
+ Accompanying path to a certificate chain. (default:
+ None)
+ --config-dir CONFIG_DIR
+ Configuration directory. (default: /etc/letsencrypt)
+ --work-dir WORK_DIR Working directory. (default: /var/lib/letsencrypt)
+ --logs-dir LOGS_DIR Logs directory. (default: /var/log/letsencrypt)
+ --server SERVER ACME Directory Resource URI. (default:
+ https://acme-v01.api.letsencrypt.org/directory)
+
+manage:
+ Various subcommands and flags are available for managing your
+ certificates:
+
+ certificates List certificates managed by Certbot
+ delete Clean up all files related to a certificate
+ renew Renew all certificates (or one specifed with --cert-
+ name)
+ revoke Revoke a certificate specified with --cert-path
+ update_symlinks Recreate symlinks in your /etc/letsencrypt/live/
+ directory
+
+run:
+ Options for obtaining & installing certs
+
+certonly:
+ Options for modifying how a cert is obtained
+
+ --csr CSR Path to a Certificate Signing Request (CSR) in DER or
+ PEM format. Currently --csr only works with the
+ 'certonly' subcommand. (default: None)
+
renew:
The 'renew' subcommand will attempt to renew all certificates (or more
precisely, certificate lineages) you have previously obtained if they are
@@ -190,14 +242,17 @@ renew:
can be used to temporarily shut down a webserver that
might conflict with the standalone plugin. This will
only be called if a certificate is actually to be
- obtained/renewed. (default: None)
+ obtained/renewed. When renewing several certificates
+ that have identical pre-hooks, only the first will be
+ executed. (default: None)
--post-hook POST_HOOK
Command to be run in a shell after attempting to
obtain/renew certificates. Can be used to deploy
renewed certificates, or to restart any servers that
were stopped by --pre-hook. This is only run if an
- attempt was made to obtain/renew a certificate.
- (default: None)
+ attempt was made to obtain/renew a certificate. If
+ multiple renewed certificates have identical post-
+ hooks, only one will be run. (default: None)
--renew-hook RENEW_HOOK
Command to be run in a shell once for each
successfully renewed certificate. For this command,
@@ -214,73 +269,69 @@ renew:
the hooks aren't being run just yet. The validation is
rather simplistic and fails if you use more advanced
shell constructs, so you can use this switch to
- disable it. (default: True)
+ disable it. (default: False)
-certonly:
- Options for modifying how a cert is obtained
+certificates:
+ List certificates managed by Certbot
- --tls-sni-01-port TLS_SNI_01_PORT
- Port used during tls-sni-01 challenge. This only
- affects the port Certbot listens on. A conforming ACME
- server will still attempt to connect on port 443.
- (default: 443)
- --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)
- --csr CSR Path to a Certificate Signing Request (CSR) in DER
- format; note that the .csr file *must* contain a
- Subject Alternative Name field for each domain you
- want certified. Currently --csr only works with the
- 'certonly' subcommand' (default: None)
-
-install:
- Options for modifying how a cert is deployed
+delete:
+ Options for deleting a certificate
revoke:
Options for revocation of certs
+register:
+ Options for account registration & modification
+
+ --register-unsafely-without-email
+ Specifying this flag enables registering an account
+ with no email address. This is strongly discouraged,
+ because in the event of key loss or account compromise
+ you will irrevocably lose access to your account. You
+ will also be unable to receive notice about impending
+ expiration or revocation of your certificates. Updates
+ to the Subscriber Agreement will still affect you, and
+ will be effective 14 days after posting an update to
+ the web site. (default: False)
+ --update-registration
+ With the register verb, indicates that details
+ associated with an existing registration, such as the
+ e-mail address, should be updated, rather than
+ registering a new account. (default: False)
+ -m EMAIL, --email EMAIL
+ Email used for registration and recovery contact.
+ (default: Ask)
+
+install:
+ Options for modifying how a cert is deployed
+
+ --fullchain-path FULLCHAIN_PATH
+ Accompanying path to a full certificate chain (cert
+ plus chain). (default: None)
+
+config_changes:
+ Options for controlling which changes are displayed
+
+ --num NUM How many past revisions you want to be displayed
+ (default: None)
+
rollback:
- Options for reverting config changes
+ Options for rolling back server configuration changes
--checkpoints N Revert configuration N number of checkpoints.
(default: 1)
plugins:
- Options for the "plugins" subcommand
+ Options for for the "plugins" subcommand
--init Initialize plugins. (default: False)
--prepare Initialize and prepare plugins. (default: False)
--authenticators Limit to authenticator plugins only. (default: None)
--installers Limit to installer plugins only. (default: None)
-config_changes:
- Options for showing a history of config changes
-
- --num NUM How many past revisions you want to be displayed
- (default: None)
-
-paths:
- Arguments changing execution paths & servers
-
- --cert-path CERT_PATH
- Path to where cert is saved (with auth --csr),
- installed from or revoked. (default: None)
- --key-path KEY_PATH Path to private key for cert installation or
- revocation (if account key is missing) (default: None)
- --fullchain-path FULLCHAIN_PATH
- Accompanying path to a full certificate chain (cert
- plus chain). (default: None)
- --chain-path CHAIN_PATH
- Accompanying path to a certificate chain. (default:
- None)
- --config-dir CONFIG_DIR
- Configuration directory. (default: /etc/letsencrypt)
- --work-dir WORK_DIR Working directory. (default: /var/lib/letsencrypt)
- --logs-dir LOGS_DIR Logs directory. (default: /var/log/letsencrypt)
- --server SERVER ACME Directory Resource URI. (default:
- https://acme-v01.api.letsencrypt.org/directory)
+update_symlinks:
+ Recreates cert 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
@@ -289,15 +340,15 @@ plugins:
provided below. Running --help will list flags specific to
that plugin.
+ --configurator CONFIGURATOR
+ Name of the plugin that is both an authenticator and
+ an installer. Should not be used together with
+ --authenticator or --installer. (default: Ask)
-a AUTHENTICATOR, --authenticator AUTHENTICATOR
Authenticator plugin name. (default: None)
-i INSTALLER, --installer INSTALLER
Installer plugin name (also used to find domains).
(default: None)
- --configurator CONFIGURATOR
- Name of the plugin that is both an authenticator and
- an installer. Should not be used together with
- --authenticator or --installer. (default: None)
--apache Obtain and install certs using Apache (default: False)
--nginx Obtain and install certs using Nginx (default: False)
--standalone Obtain certs using a "standalone" webserver. (default:
@@ -320,13 +371,24 @@ standalone:
Spin up a temporary webserver
manual:
- Manually configure an HTTP server
+ 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.
- --manual-test-mode Test mode. Executes the manual command in subprocess.
- (default: False)
+ --manual-auth-hook MANUAL_AUTH_HOOK
+ Path or command to execute for the authentication
+ script (default: None)
+ --manual-cleanup-hook MANUAL_CLEANUP_HOOK
+ Path or command to execute for the cleanup script
+ (default: None)
--manual-public-ip-logging-ok
- Automatically allows public IP logging. (default:
- False)
+ Automatically allows public IP logging (default: Ask)
webroot:
Place files in webroot directory
@@ -337,7 +399,7 @@ webroot:
domain will have the webroot path that preceded it.
For instance: `-w /var/www/example -d example.com -d
www.example.com -w /var/www/thing -d thing.net -d
- m.thing.net` (default: [])
+ m.thing.net` (default: Ask)
--webroot-map WEBROOT_MAP
JSON dictionary mapping domains to webroot paths; this
implies -d for each entry. You may need to escape this
diff --git a/docs/conf.py b/docs/conf.py
index e387e1eae..7b3f2026c 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -45,7 +45,6 @@ extensions = [
'sphinx.ext.coverage',
'sphinx.ext.viewcode',
'repoze.sphinx.autointerface',
- 'sphinxcontrib.programoutput',
]
autodoc_member_order = 'bysource'
diff --git a/docs/contributing.rst b/docs/contributing.rst
index a5b9b5688..f2129ed28 100644
--- a/docs/contributing.rst
+++ b/docs/contributing.rst
@@ -107,7 +107,7 @@ command you are required to run is::
Otherwise, please follow the following instructions.
-Mac OS X users: Run ``./tests/mac-bootstrap.sh`` instead of
+macOS users: Run ``./tests/mac-bootstrap.sh`` instead of
``boulder-start.sh`` to install dependencies, configure the
environment, and start boulder.
@@ -246,9 +246,8 @@ configuration checkpoints and rollback.
Display
~~~~~~~
-We currently offer a pythondialog and "text" mode for displays. Display
-plugins implement the `~certbot.interfaces.IDisplay`
-interface.
+We currently only offer a "text" mode for displays. Display plugins
+implement the `~certbot.interfaces.IDisplay` interface.
.. _dev-plugin:
@@ -325,6 +324,48 @@ Steps:
7. Submit the PR.
8. Did your tests pass on Travis? If they didn't, fix any errors.
+
+Updating certbot-auto and letsencrypt-auto
+==========================================
+Updating the scripts
+--------------------
+Developers should *not* modify the ``certbot-auto`` and ``letsencrypt-auto`` files
+in the root directory of the repository. Rather, modify the
+``letsencrypt-auto.template`` and associated platform-specific shell scripts in
+the ``letsencrypt-auto-source`` and
+``letsencrypt-auto-source/pieces/bootstrappers`` directory, respectively.
+
+Building letsencrypt-auto-source/letsencrypt-auto
+-------------------------------------------------
+Once changes to any of the aforementioned files have been made, the
+``letesncrypt-auto-source/letsencrypt-auto`` script should be updated. In lieu of
+manually updating this script, run the build script, which lives at
+``letsencrypt-auto-source/build.py``:
+
+.. code-block:: shell
+
+ python letsencrypt-auto-source/build.py
+
+Running ``build.py`` will update the ``letsencrypt-auto-source/letsencrypt-auto``
+script. Note that the ``certbot-auto`` and ``letsencrypt-auto`` scripts in the root
+directory of the repository will remain **unchanged** after this script is run.
+Your changes will be propagated to these files during the next release of
+Certbot.
+
+Opening a PR
+------------
+When opening a PR, ensure that the following files are committed:
+
+1. ``letsencrypt-auto-source/letsencrypt-auto.template`` and
+ ``letsencrypt-auto-source/pieces/bootstrappers/*``
+2. ``letsencrypt-auto-source/letsencrypt-auto`` (generated by ``build.py``)
+
+It might also be a good idea to double check that **no** changes were
+inadvertently made to the ``certbot-auto`` or ``letsencrypt-auto`` scripts in the
+root of the repository. These scripts will be updated by the core developers
+during the next release.
+
+
Updating the documentation
==========================
diff --git a/docs/install.rst b/docs/install.rst
index 56f6c1189..aa59e44ec 100644
--- a/docs/install.rst
+++ b/docs/install.rst
@@ -73,6 +73,23 @@ For full command line help, you can type::
./certbot-auto --help all
+Problems with Python virtual environment
+----------------------------------------
+
+On a low memory system such as VPS with only 256MB of RAM, the required dependencies of Certbot will failed to build.
+This can be identified if the pip outputs contains something like ``internal compiler error: Killed (program cc1)``.
+You can workaround this restriction by creating a temporary swapfile::
+
+ user@webserver:~$ sudo fallocate -l 1G /tmp/swapfile
+ user@webserver:~$ sudo chmod 600 /tmp/swapfile
+ user@webserver:~$ sudo mkswap /tmp/swapfile
+ user@webserver:~$ sudo swapon /tmp/swapfile
+
+Disable and remove the swapfile once the virtual enviroment is constructed::
+
+ user@webserver:~$ sudo swapoff /tmp/swapfile
+ user@webserver:~$ sudo rm /tmp/swapfile
+
Running with Docker
-------------------
@@ -114,7 +131,7 @@ For more information about the layout
of the ``/etc/letsencrypt`` directory, see :ref:`where-certs`.
.. _Docker: https://docker.com
-.. _`install Docker`: https://docs.docker.com/userguide/
+.. _`install Docker`: https://docs.docker.com/engine/installation/
Operating System Packages
-------------------------
@@ -153,13 +170,13 @@ repo, if you have not already done so. Then run:
.. code-block:: shell
- sudo apt-get install letsencrypt python-letsencrypt-apache -t jessie-backports
+ sudo apt-get install certbot python-certbot-apache -t jessie-backports
**Fedora**
.. code-block:: shell
- sudo dnf install letsencrypt
+ sudo dnf install certbot python2-certbot-apache
**Gentoo**
@@ -168,8 +185,8 @@ want to use the Apache plugin, it has to be installed separately:
.. code-block:: shell
- emerge -av app-crypt/letsencrypt
- emerge -av app-crypt/letsencrypt-apache
+ emerge -av app-crypt/certbot
+ emerge -av app-crypt/certbot-apache
When using the Apache plugin, you will run into a "cannot find a cert or key
directive" error if you're sporting the default Gentoo ``httpd.conf``.
diff --git a/docs/using.rst b/docs/using.rst
index 1becea8ea..a5493b145 100644
--- a/docs/using.rst
+++ b/docs/using.rst
@@ -55,13 +55,15 @@ standalone_ Y N | Uses a "standalone" webserver to obtain a cert. Requires
| with no webserver, or when direct integration with the local
| webserver is not supported or not desired.
manual_ Y N | Helps you obtain a cert by giving you instructions to perform http-01_ (80) or
- | domain validation yourself. dns-01_ (53)
+ | domain validation yourself. Additionally allows you to dns-01_ (53)
+ | specify scripts to automate the validation task in a
+ | customized way.
=========== ==== ==== =============================================================== =============================
Under the hood, plugins use one of several ACME protocol "Challenges_" to
prove you control a domain. The options are http-01_ (which uses port 80),
tls-sni-01_ (port 443) and dns-01_ (requring configuration of a DNS server on
-port 53, thought that's often not the same machine as your webserver). A few
+port 53, though that's often not the same machine as your webserver). A few
plugins support more than one challenge type, in which case you can choose one
with ``--preferred-challenges``.
@@ -129,7 +131,7 @@ the webserver.
Nginx
-----
-The Nginx plugin has been distributed with Cerbot since version 0.9.0 and should
+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
configurations before using it (though you can also revert changes to
configurations with ``certbot --nginx rollback``). You can use it by providing
@@ -168,6 +170,11 @@ the UI, you can use the plugin to obtain a cert by specifying
to copy and paste commands into another terminal session, which may
be on a different computer.
+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.
+
.. _third-party-plugins:
Third-party plugins
@@ -190,10 +197,11 @@ icecast_ N Y Deploy certs to Icecast 2 streaming media servers
pritunl_ N Y Install certs in pritunl distributed OpenVPN servers
proxmox_ N Y Install certs in Proxmox Virtualization servers
postfix_ N Y STARTTLS Everywhere is becoming a Certbot Postfix/Exim plugin
+heroku_ Y Y Integration with Heroku SSL
=========== ==== ==== ===============================================================
.. _plesk: https://github.com/plesk/letsencrypt-plesk
-.. _haproxy: https://code.greenhost.net/open/letsencrypt-haproxy
+.. _haproxy: https://github.com/greenhost/certbot-haproxy
.. _s3front: https://github.com/dlapiduz/letsencrypt-s3front
.. _gandi: https://github.com/Gandi/letsencrypt-gandi
.. _icecast: https://github.com/e00E/lets-encrypt-icecast
@@ -203,6 +211,7 @@ postfix_ N Y STARTTLS Everywhere is becoming a Certbot Postfix/Exim plu
.. _proxmox: https://github.com/kharkevich/letsencrypt-proxmox
.. _external: https://github.com/marcan/letsencrypt-external
.. _postfix: https://github.com/EFForg/starttls-everywhere
+.. _heroku: https://github.com/gboudreau/certbot-heroku
If you're interested, you can also :ref:`write your own plugin `.
@@ -233,7 +242,7 @@ certificate that contains all of the old domains and one or more additional
new domains.
``--allow-subset-of-names`` tells Certbot to continue with cert generation if
-only some of the specified domain authorazations can be obtained. This may
+only some of the specified domain authorizations can be obtained. This may
be useful if some domains specified in a certificate no longer point at this
system.
@@ -416,6 +425,129 @@ The following files are available:
could convert using ``openssl``. You can automate that with
``--renew-hook`` if you're using automatic renewal_.
+.. _hooks:
+
+Pre and Post Validation Hooks
+=============================
+
+Certbot allows for the specification fo pre and post validation hooks when run
+in manual mode. The flags to specify these scripts are ``--manual-auth-hook``
+and ``--manual-cleanup-hook`` respectively and can be used as such:
+
+::
+
+ 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
+variables to these scripts:
+
+- ``CERTBOT_DOMAIN``: The domain being authenticated
+- ``CERTBOT_VALIDATION``: The validation string
+- ``CERTBOT_TOKEN``: Resource name part of the HTTP-01 challenege (HTTP-01 only)
+
+Additionally for cleanup:
+
+- ``CERTBOT_AUTH_OUTPUT``: Whatever the auth script wrote to stdout
+
+Example usage for HTTP-01:
+
+::
+
+ certbot certonly --manual --preferred-challenges=http --manual-auth-hook /path/to/http/authenticator.sh --manual-cleanup-hook /path/to/http/cleanup.sh -d secure.example.com
+
+/path/to/http/authenticator.sh
+
+.. code-block:: none
+
+ #!/bin/bash
+ echo $CERTBOT_VALIDATION > /var/www/htdocs/.well-known/acme-challenge/$CERTBOT_TOKEN
+
+/path/to/http/cleanup.sh
+
+.. code-block:: none
+
+ #!/bin/bash
+ rm -f /var/www/htdocs/.well-known/acme-challenge/$CERTBOT_TOKEN
+
+Example usage for DNS-01 (Cloudflare API v4) (for example purposes only, do not use)
+
+::
+
+ certbot certonly --manual --preferred-challenges=dns --manual-auth-hook /path/to/dns/authenticator.sh --manual-cleanup-hook /path/to/dns/cleanup.sh -d secure.example.com
+
+/path/to/dns/authenticator.sh
+
+.. code-block:: none
+
+ #!/bin/bash
+
+ # Get your API key from https://www.cloudflare.com/a/account/my-account
+ API_KEY="your-api-key"
+ EMAIL="your.email@example.com"
+
+ # Strip only the top domain to get the zone id
+ DOMAIN=$(expr match "$CERTBOT_DOMAIN" '.*\.\(.*\..*\)')
+
+ # Get the Cloudflare zone id
+ ZONE_EXTRA_PARAMS="status=active&page=1&per_page=20&order=status&direction=desc&match=all"
+ ZONE_ID=$(curl -s -X GET "https://api.cloudflare.com/client/v4/zones?name=$DOMAIN&$ZONE_EXTRA_PARAMS" \
+ -H "X-Auth-Email: $EMAIL" \
+ -H "X-Auth-Key: $API_KEY" \
+ -H "Content-Type: application/json" | python -c "import sys,json;print(json.load(sys.stdin)['result'][0]['id'])")
+
+ # Create TXT record
+ CREATE_DOMAIN="_acme-challenge.$CERTBOT_DOMAIN"
+ RECORD_ID=$(curl -s -X POST "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records" \
+ -H "X-Auth-Email: $EMAIL" \
+ -H "X-Auth-Key: $API_KEY" \
+ -H "Content-Type: application/json" \
+ --data '{"type":"TXT","name":"'"$CREATE_DOMAIN"'","content":"'"$CERTBOT_VALIDATION"'","ttl":120}' \
+ | python -c "import sys,json;print(json.load(sys.stdin)['result']['id'])")
+ # Save info for cleanup
+ if [ ! -d /tmp/CERTBOT_$CERTBOT_DOMAIN ];then
+ mkdir -m 0700 /tmp/CERTBOT_$CERTBOT_DOMAIN
+ fi
+ echo $ZONE_ID > /tmp/CERTBOT_$CERTBOT_DOMAIN/ZONE_ID
+ echo $RECORD_ID > /tmp/CERTBOT_$CERTBOT_DOMAIN/RECORD_ID
+
+ # Sleep to make sure the change has time to propagate over to DNS
+ sleep 25
+
+/path/to/dns/cleanup.sh
+
+.. code-block:: none
+
+ #!/bin/bash
+
+ # Get your API key from https://www.cloudflare.com/a/account/my-account
+ API_KEY="your-api-key"
+ EMAIL="your.email@example.com"
+
+ if [ -f /tmp/CERTBOT_$CERTBOT_DOMAIN/ZONE_ID ]; then
+ ZONE_ID=$(cat /tmp/CERTBOT_$CERTBOT_DOMAIN/ZONE_ID)
+ rm -f /tmp/CERTBOT_$CERTBOT_DOMAIN/ZONE_ID
+ fi
+
+ if [ -f /tmp/CERTBOT_$CERTBOT_DOMAIN/RECORD_ID ]; then
+ RECORD_ID=$(cat /tmp/CERTBOT_$CERTBOT_DOMAIN/RECORD_ID)
+ rm -f /tmp/CERTBOT_$CERTBOT_DOMAIN/RECORD_ID
+ fi
+
+ # Remove the challenge TXT record from the zone
+ if [ -n "${ZONE_ID}" ]; then
+ if [ -n "${RECORD_ID}" ]; then
+ curl -s -X DELETE "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records/$RECORD_ID" \
+ -H "X-Auth-Email: $EMAIL" \
+ -H "X-Auth-Key: $API_KEY" \
+ -H "Content-Type: application/json"
+ fi
+ fi
+
+
+
+
+
.. _config-file:
@@ -461,5 +593,3 @@ give us as much information as possible:
- copy and paste ``certbot --version`` output
- your operating system, including specific version
- specify which installation method you've chosen
-
-
diff --git a/letsencrypt-apache/LICENSE.txt b/letsencrypt-apache/LICENSE.txt
deleted file mode 100644
index 981c46c9f..000000000
--- a/letsencrypt-apache/LICENSE.txt
+++ /dev/null
@@ -1,190 +0,0 @@
- 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/letsencrypt-apache/MANIFEST.in b/letsencrypt-apache/MANIFEST.in
deleted file mode 100644
index 97e2ad3df..000000000
--- a/letsencrypt-apache/MANIFEST.in
+++ /dev/null
@@ -1,2 +0,0 @@
-include LICENSE.txt
-include README.rst
diff --git a/letsencrypt-apache/README.rst b/letsencrypt-apache/README.rst
deleted file mode 100644
index c0c201f14..000000000
--- a/letsencrypt-apache/README.rst
+++ /dev/null
@@ -1,2 +0,0 @@
-This package is a simple shim for backwards compatibility around
-``certbot-apache``, the Apache plugin for ``certbot``.
diff --git a/letsencrypt-apache/letsencrypt_apache/__init__.py b/letsencrypt-apache/letsencrypt_apache/__init__.py
deleted file mode 100644
index cc8faef21..000000000
--- a/letsencrypt-apache/letsencrypt_apache/__init__.py
+++ /dev/null
@@ -1,8 +0,0 @@
-"""Let's Encrypt Apache plugin."""
-import sys
-
-
-import certbot_apache
-
-
-sys.modules['letsencrypt_apache'] = certbot_apache
diff --git a/letsencrypt-apache/setup.py b/letsencrypt-apache/setup.py
deleted file mode 100644
index 09703841c..000000000
--- a/letsencrypt-apache/setup.py
+++ /dev/null
@@ -1,59 +0,0 @@
-import codecs
-import os
-import sys
-
-from setuptools import setup
-from setuptools import find_packages
-
-
-def read_file(filename, encoding='utf8'):
- """Read unicode from given file."""
- with codecs.open(filename, encoding=encoding) as fd:
- return fd.read()
-
-
-here = os.path.abspath(os.path.dirname(__file__))
-readme = read_file(os.path.join(here, 'README.rst'))
-
-
-version = '0.8.0.dev0'
-
-
-# This package is a simple shim around certbot-apache
-install_requires = [
- 'certbot-apache',
- 'letsencrypt=={0}'.format(version),
-]
-
-
-setup(
- name='letsencrypt-apache',
- version=version,
- description="Apache plugin for Let's Encrypt",
- long_description=readme,
- url='https://github.com/letsencrypt/letsencrypt',
- 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',
- '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,
-)
diff --git a/letsencrypt-auto b/letsencrypt-auto
index cba185eae..a2ddf76ac 100755
--- a/letsencrypt-auto
+++ b/letsencrypt-auto
@@ -15,11 +15,15 @@ set -e # Work even if somebody does "sh thisscript.sh".
# Note: you can set XDG_DATA_HOME or VENV_PATH before running this script,
# if you want to change where the virtual environment will be installed
-XDG_DATA_HOME=${XDG_DATA_HOME:-~/.local/share}
+if [ -z "$XDG_DATA_HOME" ]; then
+ XDG_DATA_HOME=~/.local/share
+fi
VENV_NAME="letsencrypt"
-VENV_PATH=${VENV_PATH:-"$XDG_DATA_HOME/$VENV_NAME"}
+if [ -z "$VENV_PATH" ]; then
+ VENV_PATH="$XDG_DATA_HOME/$VENV_NAME"
+fi
VENV_BIN="$VENV_PATH/bin"
-LE_AUTO_VERSION="0.9.3"
+LE_AUTO_VERSION="0.10.0"
BASENAME=$(basename $0)
USAGE="Usage: $BASENAME [OPTIONS]
A self-updating wrapper script for the Certbot ACME client. When run, updates
@@ -58,6 +62,7 @@ for arg in "$@" ; do
--verbose)
VERBOSE=1;;
-[!-]*)
+ OPTIND=1
while getopts ":hnvq" short_arg $arg; do
case "$short_arg" in
h)
@@ -79,43 +84,74 @@ if [ $BASENAME = "letsencrypt-auto" ]; then
HELP=0
fi
+# Support for busybox and others where there is no "command",
+# but "which" instead
+if command -v command > /dev/null 2>&1 ; then
+ export EXISTS="command -v"
+elif which which > /dev/null 2>&1 ; then
+ export EXISTS="which"
+else
+ echo "Cannot find command nor which... please install one!"
+ exit 1
+fi
+
# certbot-auto needs root access to bootstrap OS dependencies, and
# certbot itself needs root access for almost all modes of operation
# The "normal" case is that sudo is used for the steps that need root, but
# this script *can* be run as root (not recommended), or fall back to using
-# `su`
+# `su`. Auto-detection can be overrided by explicitly setting the
+# environment variable LE_AUTO_SUDO to 'sudo', 'sudo_su' or '' as used below.
+
+# Because the parameters in `su -c` has to be a string,
+# we need to properly escape it.
+su_sudo() {
+ args=""
+ # This `while` loop iterates over all parameters given to this function.
+ # For each parameter, all `'` will be replace by `'"'"'`, and the escaped string
+ # will be wrapped in a pair of `'`, then appended to `$args` string
+ # For example, `echo "It's only 1\$\!"` will be escaped to:
+ # 'echo' 'It'"'"'s only 1$!'
+ # │ │└┼┘│
+ # │ │ │ └── `'s only 1$!'` the literal string
+ # │ │ └── `\"'\"` is a single quote (as a string)
+ # │ └── `'It'`, to be concatenated with the strings following it
+ # └── `echo` wrapped in a pair of `'`, it's totally fine for the shell command itself
+ while [ $# -ne 0 ]; do
+ args="$args'$(printf "%s" "$1" | sed -e "s/'/'\"'\"'/g")' "
+ shift
+ done
+ su root -c "$args"
+}
+
SUDO_ENV=""
export CERTBOT_AUTO="$0"
-if test "`id -u`" -ne "0" ; then
- if command -v sudo 1>/dev/null 2>&1; then
- SUDO=sudo
- SUDO_ENV="CERTBOT_AUTO=$0"
- else
- echo \"sudo\" is not available, will use \"su\" for installation steps...
- # Because the parameters in `su -c` has to be a string,
- # we need properly escape it
- su_sudo() {
- args=""
- # This `while` loop iterates over all parameters given to this function.
- # For each parameter, all `'` will be replace by `'"'"'`, and the escaped string
- # will be wrapped in a pair of `'`, then appended to `$args` string
- # For example, `echo "It's only 1\$\!"` will be escaped to:
- # 'echo' 'It'"'"'s only 1$!'
- # │ │└┼┘│
- # │ │ │ └── `'s only 1$!'` the literal string
- # │ │ └── `\"'\"` is a single quote (as a string)
- # │ └── `'It'`, to be concatenated with the strings following it
- # └── `echo` wrapped in a pair of `'`, it's totally fine for the shell command itself
- while [ $# -ne 0 ]; do
- args="$args'$(printf "%s" "$1" | sed -e "s/'/'\"'\"'/g")' "
- shift
- done
- su root -c "$args"
- }
- SUDO=su_sudo
- fi
+if [ -n "${LE_AUTO_SUDO+x}" ]; then
+ case "$LE_AUTO_SUDO" in
+ su_sudo|su)
+ SUDO=su_sudo
+ ;;
+ sudo)
+ SUDO=sudo
+ SUDO_ENV="CERTBOT_AUTO=$0"
+ ;;
+ '') ;; # Nothing to do for plain root method.
+ *)
+ echo "Error: unknown root authorization mechanism '$LE_AUTO_SUDO'."
+ exit 1
+ esac
+ echo "Using preset root authorization mechanism '$LE_AUTO_SUDO'."
else
- SUDO=
+ if test "`id -u`" -ne "0" ; then
+ if $EXISTS sudo 1>/dev/null 2>&1; then
+ SUDO=sudo
+ SUDO_ENV="CERTBOT_AUTO=$0"
+ else
+ echo \"sudo\" is not available, will use \"su\" for installation steps...
+ SUDO=su_sudo
+ fi
+ else
+ SUDO=
+ fi
fi
ExperimentalBootstrap() {
@@ -136,7 +172,7 @@ ExperimentalBootstrap() {
DeterminePythonVersion() {
for LE_PYTHON in "$LE_PYTHON" python2.7 python27 python2 python; do
# Break (while keeping the LE_PYTHON value) if found.
- command -v "$LE_PYTHON" > /dev/null && break
+ $EXISTS "$LE_PYTHON" > /dev/null && break
done
if [ "$?" != "0" ]; then
echo "Cannot find any Pythons; please install one!"
@@ -177,19 +213,22 @@ BootstrapDebCommon() {
# distro version (#346)
virtualenv=
- if apt-cache show virtualenv > /dev/null 2>&1 && ! apt-cache --quiet=0 show virtualenv 2>&1 | grep -q 'No packages found'; then
- virtualenv="virtualenv"
+ # virtual env is known to apt and is installable
+ if apt-cache show virtualenv > /dev/null 2>&1 ; then
+ if ! LC_ALL=C apt-cache --quiet=0 show virtualenv 2>&1 | grep -q 'No packages found'; then
+ virtualenv="virtualenv"
+ fi
fi
if apt-cache show python-virtualenv > /dev/null 2>&1; then
- virtualenv="$virtualenv python-virtualenv"
+ virtualenv="$virtualenv python-virtualenv"
fi
augeas_pkg="libaugeas0 augeas-lenses"
- AUGVERSION=`apt-cache show --no-all-versions libaugeas0 | grep ^Version: | cut -d" " -f2`
+ AUGVERSION=`LC_ALL=C apt-cache show --no-all-versions libaugeas0 | grep ^Version: | cut -d" " -f2`
if [ "$ASSUME_YES" = 1 ]; then
- YES_FLAG="-y"
+ YES_FLAG="-y"
fi
AddBackportRepo() {
@@ -248,15 +287,15 @@ BootstrapDebCommon() {
python-dev \
$virtualenv \
gcc \
- dialog \
$augeas_pkg \
libssl-dev \
+ openssl \
libffi-dev \
ca-certificates \
- if ! command -v virtualenv > /dev/null ; then
+ if ! $EXISTS virtualenv > /dev/null ; then
echo Failed to install a working \"virtualenv\" command, exiting
exit 1
fi
@@ -307,7 +346,6 @@ BootstrapRpmCommon() {
pkgs="
gcc
- dialog
augeas-libs
openssl
openssl-devel
@@ -361,7 +399,6 @@ BootstrapSuseCommon() {
python-devel \
python-virtualenv \
gcc \
- dialog \
augeas-lenses \
libopenssl-devel \
libffi-devel \
@@ -380,7 +417,6 @@ BootstrapArchCommon() {
python2
python-virtualenv
gcc
- dialog
augeas
openssl
libffi
@@ -404,22 +440,26 @@ BootstrapGentooCommon() {
PACKAGES="
dev-lang/python:2.7
dev-python/virtualenv
- dev-util/dialog
app-admin/augeas
dev-libs/openssl
dev-libs/libffi
app-misc/ca-certificates
virtual/pkgconfig"
+ ASK_OPTION="--ask"
+ if [ "$ASSUME_YES" = 1 ]; then
+ ASK_OPTION=""
+ fi
+
case "$PACKAGE_MANAGER" in
(paludis)
$SUDO cave resolve --preserve-world --keep-targets if-possible $PACKAGES -x
;;
(pkgcore)
- $SUDO pmerge --noreplace --oneshot $PACKAGES
+ $SUDO pmerge --noreplace --oneshot $ASK_OPTION $PACKAGES
;;
(portage|*)
- $SUDO emerge --noreplace --oneshot $PACKAGES
+ $SUDO emerge --noreplace --oneshot $ASK_OPTION $PACKAGES
;;
esac
}
@@ -449,7 +489,6 @@ BootstrapMac() {
fi
$pkgcmd augeas
- $pkgcmd dialog
if [ "$(which python)" = "/System/Library/Frameworks/Python.framework/Versions/2.7/bin/python" \
-o "$(which python)" = "/usr/bin/python" ]; then
# We want to avoid using the system Python because it requires root to use pip.
@@ -458,7 +497,7 @@ BootstrapMac() {
$pkgcmd python
fi
- # Workaround for _dlopen not finding augeas on OS X
+ # Workaround for _dlopen not finding augeas on macOS
if [ "$pkgman" = "port" ] && ! [ -e "/usr/local/lib/libaugeas.dylib" ] && [ -e "/opt/local/lib/libaugeas.dylib" ]; then
echo "Applying augeas workaround"
$SUDO mkdir -p /usr/local/lib/
@@ -496,8 +535,8 @@ BootstrapMageiaCommon() {
if ! $SUDO urpmi --force \
git \
gcc \
- cdialog \
python-augeas \
+ openssl \
libopenssl-devel \
libffi-devel \
rootcerts
@@ -541,7 +580,7 @@ Bootstrap() {
elif uname | grep -iq FreeBSD ; then
ExperimentalBootstrap "FreeBSD" BootstrapFreeBsd
elif uname | grep -iq Darwin ; then
- ExperimentalBootstrap "Mac OS X" BootstrapMac
+ ExperimentalBootstrap "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
@@ -557,7 +596,7 @@ Bootstrap() {
}
TempDir() {
- mktemp -d 2>/dev/null || mktemp -d -t 'le' # Linux || OS X
+ mktemp -d 2>/dev/null || mktemp -d -t 'le' # Linux || macOS
}
@@ -594,6 +633,11 @@ if [ "$1" = "--le-auto-phase2" ]; then
# `pip install --no-cache-dir -e acme -e . -e certbot-apache -e certbot-nginx`,
# and then use `hashin` or a more secure method to gather the hashes.
+# Hashin example:
+# pip install hashin
+# hashin -r letsencrypt-auto-requirements.txt cryptography==1.5.2
+# sets the new certbot-auto pinned version of cryptography to 1.5.2
+
argparse==1.4.0 \
--hash=sha256:c31647edb69fd3d465a847ea3157d37bed1f95f19760b11a47aa91c04b666314 \
--hash=sha256:62b089a55be1d8949cd2bc7e0df0bddb9e028faefc8c32038cc84862aefdd6e4
@@ -601,7 +645,8 @@ argparse==1.4.0 \
# This comes before cffi because cffi will otherwise install an unchecked
# version via setup_requires.
pycparser==2.14 \
- --hash=sha256:7959b4a74abdc27b312fed1c21e6caf9309ce0b29ea86b591fd2e99ecdf27f73
+ --hash=sha256:7959b4a74abdc27b312fed1c21e6caf9309ce0b29ea86b591fd2e99ecdf27f73 \
+ --no-binary pycparser
cffi==1.4.2 \
--hash=sha256:53c1c9ddb30431513eb7f3cdef0a3e06b0f1252188aaa7744af0f5a4cd45dbaf \
@@ -624,29 +669,29 @@ ConfigArgParse==0.10.0 \
--hash=sha256:3b50a83dd58149dfcee98cb6565265d10b53e9c0a2bca7eeef7fb5f5524890a7
configobj==5.0.6 \
--hash=sha256:a2f5650770e1c87fb335af19a9b7eb73fc05ccf22144eb68db7d00cd2bcb0902
-cryptography==1.3.4 \
- --hash=sha256:bede00edd11a2a62c8c98c271cc103fa3a3d72acf64f6e5e4eaf251128897b17 \
- --hash=sha256:53b39e687b744bb548a98f40736cc529d9f60959b4e6cc551322cf9505d35eb3 \
- --hash=sha256:474b73ad1139b4e423e46bbd818efd0d5c0df1c65d9f7c957d64c9215d77afde \
- --hash=sha256:aaddf9592d5b99e32dd518bb4a25b147c124f9d6b4ad64b94f01b15d1666b8c8 \
- --hash=sha256:6dcad2f407db8c3cd6ecd78361439c449a4f94786b46c54507e7e68f51e1709d \
- --hash=sha256:475c153fc622e656f1f10a9c9941d0ac7ab18df7c38d35d563a437c1c0e34f24 \
- --hash=sha256:86dd61df581cba04e89e45081efbc531faff1c9d99c77b1ce97f87216c356353 \
- --hash=sha256:75cc697e4ef5fdd0102ca749114c6370dbd11db0c9132a18834858c2566247e3 \
- --hash=sha256:ea03ad5b9df6d79fc9fc1ab23729e01e1c920d2974c5e3c634ccf45a5c378452 \
- --hash=sha256:c8872b8fe4f3416d6338ab99612f49ab314f7856cb43bffab2a32d28a6267be8 \
- --hash=sha256:468fc6e16eaec6ceaa6bc341273e6e9912d01b42b740f8cf896ace7fcd6a321d \
- --hash=sha256:d6fea3c6502735011c5d61a62aef1c1d770fc6a2def45d9e6c0d94c9651e3317 \
- --hash=sha256:3cf95f179f4bead3d5649b91860ef4cf60ad4244209190fc405908272576d961 \
- --hash=sha256:141f77e60a5b9158309b2b60288c7f81d37faa15c22a69b94c190ceefaaa6236 \
- --hash=sha256:87b7a1fe703c6424451f3372d1879dae91c7fe5e13375441a72833db76fee30e \
- --hash=sha256:f5ee3cb0cf1a6550bf483ccffa6608db267a377b45f7e3a8201a86d1d8feb19f \
- --hash=sha256:4e097286651ea318300af3251375d48b71b8228481c56cd617ddd4459a1ff261 \
- --hash=sha256:1e3d3ae3f22f22d50d340f47f25227511326f3f1396c6d2446a5b45b516c4313 \
- --hash=sha256:6a057941cb64d79834ea3cf99093fcc4787c2a5d44f686c4f297361ddc419bcd \
- --hash=sha256:68b3d5390b92559ddd3353c73ab2dfcff758f9c4ec4f5d5226ccede0e5d779f4 \
- --hash=sha256:545dc003b4b6081f9c3e452da15d819b04b696f49484aff64c0a2aedf766bef8 \
- --hash=sha256:423ff890c01be7c70dbfeaa967eeef5146f1a43a5f810ffdc07b178e48a105a9
+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
enum34==1.1.2 \
--hash=sha256:2475d7fcddf5951e92ff546972758802de5260bf409319a9f1934e6bbc8b1dc7 \
--hash=sha256:35907defb0f992b75ab7788f65fedc1cf20ffa22688e0e6f6f12afc06b3ea501
@@ -662,8 +707,6 @@ ipaddress==1.0.16 \
linecache2==1.0.0 \
--hash=sha256:e78be9c0a0dfcbac712fe04fbf92b96cddae80b1b842f24248214c8496f006ef \
--hash=sha256:4b26ff4e7110db76eeb6f5a7b64a82623839d595c2038eeda662f2a2db78e97c
-ndg-httpsclient==0.4.0 \
- --hash=sha256:e8c155fdebd9c4bcb0810b4ed01ae1987554b1ee034dd7532d7b8fdae38a6274
ordereddict==1.1 \
--hash=sha256:1c35b4ac206cef2d24816c89f89cf289dd3d38cf7c449bb3fab7bf6d43f01b1f
parsedatetime==2.1 \
@@ -684,9 +727,9 @@ pyasn1==0.1.9 \
--hash=sha256:5191ff6b9126d2c039dd87f8ff025bed274baf07fa78afa46f556b1ad7265d6e \
--hash=sha256:8323e03637b2d072cc7041300bac6ec448c3c28950ab40376036788e9a1af629 \
--hash=sha256:853cacd96d1f701ddd67aa03ecc05f51890135b7262e922710112f12a2ed2a7f
-pyopenssl==16.0.0 \
- --hash=sha256:5add70cf00273bf957ca31fdb0df9b0ae4639e081897d5f86a0ae1f104901230 \
- --hash=sha256:363d10ee43d062285facf4e465f4f5163f9f702f9134f0a5896f134cbb92d17d
+pyOpenSSL==16.2.0 \
+ --hash=sha256:26ca380ddf272f7556e48064bbcd5bd71f83dfc144f3583501c7ddbd9434ee17 \
+ --hash=sha256:7779a3bbb74e79db234af6a08775568c6769b5821faecf6e2f4143edb227516e
pyparsing==2.1.8 \
--hash=sha256:2f0f5ceb14eccd5aef809d6382e87df22ca1da583c79f6db01675ce7d7f49c18 \
--hash=sha256:03a4869b9f3493807ee1f1cb405e6d576a1a2ca4d81a982677c0c1ad6177c56b \
@@ -701,9 +744,6 @@ pyRFC3339==1.0 \
--hash=sha256:8dfbc6c458b8daba1c0f3620a8c78008b323a268b27b7359e92a4ae41325f535
python-augeas==0.5.0 \
--hash=sha256:67d59d66cdba8d624e0389b87b2a83a176f21f16a87553b50f5703b23f29bac2
-python2-pythondialog==3.3.0 \
- --hash=sha256:04e93f24995c43dd90f338d5d865ca72ce3fb5a5358d4daa4965571db35fc3ec \
- --hash=sha256:3e6f593fead98f8a526bc3e306933533236e33729f552f52896ea504f55313fa
pytz==2015.7 \
--hash=sha256:3abe6a6d3fc2fbbe4c60144211f45da2edbe3182a6f6511af6bbba0598b1f992 \
--hash=sha256:939ef9c1e1224d980405689a97ffcf7828c56d1517b31d73464356c1f2b7769e \
@@ -718,9 +758,9 @@ pytz==2015.7 \
--hash=sha256:fbd26746772c24cb93c8b97cbdad5cb9e46c86bbdb1b9d8a743ee00e2fb1fc5d \
--hash=sha256:99266ef30a37e43932deec2b7ca73e83c8dbc3b9ff703ec73eca6b1dae6befea \
--hash=sha256:8b6ce1c993909783bc96e0b4f34ea223bff7a4df2c90bdb9c4e0f1ac928689e3
-requests==2.9.1 \
- --hash=sha256:113fbba5531a9e34945b7d36b33a084e8ba5d0664b703c81a7c572d91919a5b8 \
- --hash=sha256:c577815dd00f1394203fc44eb979724b098f88264a9ef898ee45b8e5e9cf587f
+requests==2.12.1 \
+ --hash=sha256:3f3f27a9d0f9092935efc78054ef324eb9f8166718270aefe036dfa1e4f68e1e \
+ --hash=sha256:2109ecea94df90980be040490ff1d879971b024861539abb00054062388b612e
six==1.10.0 \
--hash=sha256:0ff78c403d9bccf5a425a6d31a12aa6b47f1c21ca4dc2573a7e2f32a97335eb1 \
--hash=sha256:105f8d68616f8248e24bf0e9372ef04d3cc10104f1980f54d57b2ce73a5ad56a
@@ -761,18 +801,18 @@ letsencrypt==0.7.0 \
# THE LINES BELOW ARE EDITED BY THE RELEASE SCRIPT; ADD ALL DEPENDENCIES ABOVE.
-acme==0.9.3 \
- --hash=sha256:d18ce17a75ad24d27981dfaef0524aa905eab757b267e027162b56a8967ab8fb \
- --hash=sha256:a6eff1f955eb2e4316abd9aa2fedb6d9345e6b5b8a2d64ea0ad35e05d6124099
-certbot==0.9.3 \
- --hash=sha256:a87ef4c53c018df4e52ee2f2e906ad16bbb37789f29e6f284c495a2eb4d9b243 \
- --hash=sha256:68149cb8392b29f5d5246e7226d25f913f2b10482bf3bc7368e8c8821d25f3b0
-certbot-apache==0.9.3 \
- --hash=sha256:f379b1053e10709692654d7a6fcea9eaed19b66c49a753b61e31bd06a04b0aac \
- --hash=sha256:a5d98cf972072de08f984db4e6a7f20269f3f023c43f6d4e781fe43be7c10086
-certbot-nginx==0.9.3 \
- --hash=sha256:3c26f18f0b57550f069263bd9b2984ef33eab6693e7796611c1b2cc16574069c \
- --hash=sha256:7337a2e90e0b28a1ab09e31d9fb81c6d78e6453500c824c0f18bab5d31b63058
+acme==0.10.0 \
+ --hash=sha256:df4299a9881d94185a1578ed97334430a90f761ce815edd300860ca47d0538f1 \
+ --hash=sha256:ddebdf1fe139c8fedbcf633955ec867496d2f7d2d2e9879d538437a69ab47876
+certbot==0.10.0 \
+ --hash=sha256:fb1bfa3d54ce9366758e374f7ed99667ce20484224934d3e8e57839fcf784bc5 \
+ --hash=sha256:dd64ed8fb3cc3b053f05e779b934433445918668c49bcdbb2c816062815e1661
+certbot-apache==0.10.0 \
+ --hash=sha256:909d59c53507093f838f7336f75d7d78563a35b16afdf6c30f45c9f47bf069da \
+ --hash=sha256:6f110dae227dd0fea9572fa12dd60b041e391f5d2028cc2e1fedd2a9a0d2bc88
+certbot-nginx==0.10.0 \
+ --hash=sha256:4f33a230d420cbd0431e7b707fb9a1732bfd18d3c6056019591bd7c3a13abe92 \
+ --hash=sha256:c12ffd05207b0be3c765b3d3e2927e0b2cc2b7de20654b19d154a0d789e7c1d5
UNLIKELY_EOF
# -------------------------------------------------------------------------
@@ -940,7 +980,28 @@ UNLIKELY_EOF
# Report error. (Otherwise, be quiet.)
echo "Had a problem while installing Python packages."
if [ "$VERBOSE" != 1 ]; then
+ echo
+ echo "pip prints the following errors: "
+ echo "====================================================="
echo "$PIP_OUT"
+ echo "====================================================="
+ echo
+ echo "Certbot has problem setting up the virtual environment."
+
+ if `echo $PIP_OUT | grep -q Killed` || `echo $PIP_OUT | grep -q "allocate memory"` ; then
+ echo
+ echo "Based on your pip output, the problem can likely be fixed by "
+ echo "increasing the available memory."
+ else
+ echo
+ echo "We were not be able to guess the right solution from your pip "
+ echo "output."
+ fi
+
+ echo
+ echo "Consult https://certbot.eff.org/docs/install.html#problems-with-python-virtual-environment"
+ echo "for possible solutions."
+ echo "You may also find some support resources at https://certbot.eff.org/support/ ."
fi
rm -rf "$VENV_PATH"
exit 1
@@ -1132,7 +1193,7 @@ UNLIKELY_EOF
# TODO: Deal with quotes in pathnames.
echo "Replacing certbot-auto..."
# Clone permissions with cp. chmod and chown don't have a --reference
- # option on OS X or BSD, and stat -c on Linux is stat -f on OS X and BSD:
+ # option on macOS or BSD, and stat -c on Linux is stat -f on macOS and BSD:
$SUDO cp -p "$0" "$TEMP_DIR/letsencrypt-auto.permission-clone"
$SUDO cp "$TEMP_DIR/letsencrypt-auto" "$TEMP_DIR/letsencrypt-auto.permission-clone"
# Using mv rather than cp leaves the old file descriptor pointing to the
diff --git a/letsencrypt-auto-source/Dockerfile.centos6 b/letsencrypt-auto-source/Dockerfile.centos6
new file mode 100644
index 000000000..e1280109b
--- /dev/null
+++ b/letsencrypt-auto-source/Dockerfile.centos6
@@ -0,0 +1,32 @@
+# For running tests, build a docker image with a passwordless sudo and a trust
+# store we can manipulate.
+
+FROM centos:6
+
+RUN yum install -y epel-release
+
+# Install pip, sudo and nose:
+RUN yum install -y python-pip sudo
+RUN pip install nose
+
+# Add an unprivileged user:
+RUN useradd --create-home --home-dir /home/lea --shell /bin/bash --groups wheel --uid 1000 lea
+
+# Let that user sudo:
+RUN sed -i.bkp -e \
+ 's/# %wheel\(NOPASSWD: ALL\)\?/%wheel/g' \
+ /etc/sudoers
+
+RUN mkdir -p /home/lea/certbot
+
+# Install fake testing CA:
+COPY ./tests/certs/ca/my-root-ca.crt.pem /usr/local/share/ca-certificates/
+RUN update-ca-trust
+
+# Copy code:
+COPY . /home/lea/certbot/letsencrypt-auto-source
+
+USER lea
+WORKDIR /home/lea
+
+CMD ["nosetests", "-v", "-s", "certbot/letsencrypt-auto-source/tests"]
diff --git a/letsencrypt-auto-source/Dockerfile.precise b/letsencrypt-auto-source/Dockerfile.precise
new file mode 100644
index 000000000..c8b593774
--- /dev/null
+++ b/letsencrypt-auto-source/Dockerfile.precise
@@ -0,0 +1,31 @@
+# For running tests, build a docker image with a passwordless sudo and a trust
+# store we can manipulate.
+
+FROM ubuntu:precise
+
+# Add an unprivileged user:
+RUN useradd --create-home --home-dir /home/lea --shell /bin/bash --groups sudo --uid 1000 lea
+
+# Install pip, sudo, openssl, and nose:
+RUN apt-get update && \
+ apt-get -q -y install python-pip sudo openssl && \
+ apt-get clean
+RUN pip install nose
+
+# Let that user sudo:
+RUN sed -i.bkp -e \
+ 's/%sudo\s\+ALL=(ALL\(:ALL\)\?)\s\+ALL/%sudo ALL=NOPASSWD:ALL/g' \
+ /etc/sudoers
+
+RUN mkdir -p /home/lea/certbot
+
+# Install fake testing CA:
+COPY ./tests/certs/ca/my-root-ca.crt.pem /usr/local/share/ca-certificates/
+
+# Copy code:
+COPY . /home/lea/certbot/letsencrypt-auto-source
+
+USER lea
+WORKDIR /home/lea
+
+CMD ["nosetests", "-v", "-s", "certbot/letsencrypt-auto-source/tests"]
diff --git a/letsencrypt-auto-source/Dockerfile b/letsencrypt-auto-source/Dockerfile.trusty
similarity index 100%
rename from letsencrypt-auto-source/Dockerfile
rename to letsencrypt-auto-source/Dockerfile.trusty
diff --git a/letsencrypt-auto-source/Dockerfile.wheezy b/letsencrypt-auto-source/Dockerfile.wheezy
new file mode 100644
index 000000000..f86795e08
--- /dev/null
+++ b/letsencrypt-auto-source/Dockerfile.wheezy
@@ -0,0 +1,31 @@
+# For running tests, build a docker image with a passwordless sudo and a trust
+# store we can manipulate.
+
+FROM debian:wheezy
+
+# Add an unprivileged user:
+RUN useradd --create-home --home-dir /home/lea --shell /bin/bash --groups sudo --uid 1000 lea
+
+# Install pip, sudo, openssl, and nose:
+RUN apt-get update && \
+ apt-get -q -y install python-pip sudo openssl && \
+ apt-get clean
+RUN pip install nose
+
+# Let that user sudo:
+RUN sed -i.bkp -e \
+ 's/%sudo\s\+ALL=(ALL\(:ALL\)\?)\s\+ALL/%sudo ALL=NOPASSWD:ALL/g' \
+ /etc/sudoers
+
+RUN mkdir -p /home/lea/certbot
+
+# Install fake testing CA:
+COPY ./tests/certs/ca/my-root-ca.crt.pem /usr/local/share/ca-certificates/
+
+# Copy code:
+COPY . /home/lea/certbot/letsencrypt-auto-source
+
+USER lea
+WORKDIR /home/lea
+
+CMD ["nosetests", "-v", "-s", "certbot/letsencrypt-auto-source/tests"]
diff --git a/letsencrypt-auto-source/certbot-auto.asc b/letsencrypt-auto-source/certbot-auto.asc
index db40cfb84..2dfa27621 100644
--- a/letsencrypt-auto-source/certbot-auto.asc
+++ b/letsencrypt-auto-source/certbot-auto.asc
@@ -1,11 +1,11 @@
-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1
-iQEcBAABAgAGBQJYADL6AAoJEE0XyZXNl3XyZW8H/RgPxga4SZ8VoMGGOpzYGzaD
-C/VW6IZeHjD7urkAjfSiMMStkYKlZMGcT/3Pw1L39wIX/37jqQTTh01JL+TcqRMJ
-AUHmSgrErjUU42YV68u2c/wT9Dsid+OxpP/WSbJn5MomWtvGpFxffc/FK/W8ccFR
-r6ZhAt2rgkBmYjrC6w8V9KTzhp4+n7ZpQPxuMFxpJhyTmMzgj9K+aI2OuKDKT7iO
-nke74Lgx/xPatLDgygw5bRiFyZ+X65p/awalEXBcFW0zmlN2Fqp8om8UjtUtkVw9
-ixr9/kq9VhcHjho9cmKWl14IShbcxZZc60xL2y6gmkgoBpzVlHfvRNnxapodTsc=
-=jULW
+iQEcBAABAgAGBQJYdmhCAAoJEE0XyZXNl3XyuSMH/i6+2GqLh00I+VQRUUHmY/CE
+PeUmrkN2N6DEFZK6Y6r7vR1QoY8xYEbmMZNmCYU+YRiO/TO3mLLycd48vbQoyttL
+Bi4JalkfkLgfNZNLYvlrDE5K7LaHIiPxQfHN2RIZS4ez6eMREyQXhTPq5HGqQuQH
+KkiC9CCKrLvmZXOZA+8ayvoo3U3SI1bZNu7d7c4pEDtkGRMZhNSs8Eejo+knDlny
+KmEVrvakkcYTeGwz+SckY9Z7rQGyYoFr2+N3owMT40/g9ZnzkaTS/y+G2z1EnWkN
+lapwugl9Pnl6Hog+SBH+osONdg04tIiNayPq11NgWNmMvbG6Lbi4p+RVg+16E1M=
+=BXeZ
-----END PGP SIGNATURE-----
diff --git a/letsencrypt-auto-source/letsencrypt-auto b/letsencrypt-auto-source/letsencrypt-auto
index b5adeea86..409d68322 100755
--- a/letsencrypt-auto-source/letsencrypt-auto
+++ b/letsencrypt-auto-source/letsencrypt-auto
@@ -15,11 +15,15 @@ set -e # Work even if somebody does "sh thisscript.sh".
# Note: you can set XDG_DATA_HOME or VENV_PATH before running this script,
# if you want to change where the virtual environment will be installed
-XDG_DATA_HOME=${XDG_DATA_HOME:-~/.local/share}
+if [ -z "$XDG_DATA_HOME" ]; then
+ XDG_DATA_HOME=~/.local/share
+fi
VENV_NAME="letsencrypt"
-VENV_PATH=${VENV_PATH:-"$XDG_DATA_HOME/$VENV_NAME"}
+if [ -z "$VENV_PATH" ]; then
+ VENV_PATH="$XDG_DATA_HOME/$VENV_NAME"
+fi
VENV_BIN="$VENV_PATH/bin"
-LE_AUTO_VERSION="0.10.0.dev0"
+LE_AUTO_VERSION="0.11.0.dev0"
BASENAME=$(basename $0)
USAGE="Usage: $BASENAME [OPTIONS]
A self-updating wrapper script for the Certbot ACME client. When run, updates
@@ -58,6 +62,7 @@ for arg in "$@" ; do
--verbose)
VERBOSE=1;;
-[!-]*)
+ OPTIND=1
while getopts ":hnvq" short_arg $arg; do
case "$short_arg" in
h)
@@ -79,43 +84,74 @@ if [ $BASENAME = "letsencrypt-auto" ]; then
HELP=0
fi
+# Support for busybox and others where there is no "command",
+# but "which" instead
+if command -v command > /dev/null 2>&1 ; then
+ export EXISTS="command -v"
+elif which which > /dev/null 2>&1 ; then
+ export EXISTS="which"
+else
+ echo "Cannot find command nor which... please install one!"
+ exit 1
+fi
+
# certbot-auto needs root access to bootstrap OS dependencies, and
# certbot itself needs root access for almost all modes of operation
# The "normal" case is that sudo is used for the steps that need root, but
# this script *can* be run as root (not recommended), or fall back to using
-# `su`
+# `su`. Auto-detection can be overrided by explicitly setting the
+# environment variable LE_AUTO_SUDO to 'sudo', 'sudo_su' or '' as used below.
+
+# Because the parameters in `su -c` has to be a string,
+# we need to properly escape it.
+su_sudo() {
+ args=""
+ # This `while` loop iterates over all parameters given to this function.
+ # For each parameter, all `'` will be replace by `'"'"'`, and the escaped string
+ # will be wrapped in a pair of `'`, then appended to `$args` string
+ # For example, `echo "It's only 1\$\!"` will be escaped to:
+ # 'echo' 'It'"'"'s only 1$!'
+ # │ │└┼┘│
+ # │ │ │ └── `'s only 1$!'` the literal string
+ # │ │ └── `\"'\"` is a single quote (as a string)
+ # │ └── `'It'`, to be concatenated with the strings following it
+ # └── `echo` wrapped in a pair of `'`, it's totally fine for the shell command itself
+ while [ $# -ne 0 ]; do
+ args="$args'$(printf "%s" "$1" | sed -e "s/'/'\"'\"'/g")' "
+ shift
+ done
+ su root -c "$args"
+}
+
SUDO_ENV=""
export CERTBOT_AUTO="$0"
-if test "`id -u`" -ne "0" ; then
- if command -v sudo 1>/dev/null 2>&1; then
- SUDO=sudo
- SUDO_ENV="CERTBOT_AUTO=$0"
- else
- echo \"sudo\" is not available, will use \"su\" for installation steps...
- # Because the parameters in `su -c` has to be a string,
- # we need properly escape it
- su_sudo() {
- args=""
- # This `while` loop iterates over all parameters given to this function.
- # For each parameter, all `'` will be replace by `'"'"'`, and the escaped string
- # will be wrapped in a pair of `'`, then appended to `$args` string
- # For example, `echo "It's only 1\$\!"` will be escaped to:
- # 'echo' 'It'"'"'s only 1$!'
- # │ │└┼┘│
- # │ │ │ └── `'s only 1$!'` the literal string
- # │ │ └── `\"'\"` is a single quote (as a string)
- # │ └── `'It'`, to be concatenated with the strings following it
- # └── `echo` wrapped in a pair of `'`, it's totally fine for the shell command itself
- while [ $# -ne 0 ]; do
- args="$args'$(printf "%s" "$1" | sed -e "s/'/'\"'\"'/g")' "
- shift
- done
- su root -c "$args"
- }
- SUDO=su_sudo
- fi
+if [ -n "${LE_AUTO_SUDO+x}" ]; then
+ case "$LE_AUTO_SUDO" in
+ su_sudo|su)
+ SUDO=su_sudo
+ ;;
+ sudo)
+ SUDO=sudo
+ SUDO_ENV="CERTBOT_AUTO=$0"
+ ;;
+ '') ;; # Nothing to do for plain root method.
+ *)
+ echo "Error: unknown root authorization mechanism '$LE_AUTO_SUDO'."
+ exit 1
+ esac
+ echo "Using preset root authorization mechanism '$LE_AUTO_SUDO'."
else
- SUDO=
+ if test "`id -u`" -ne "0" ; then
+ if $EXISTS sudo 1>/dev/null 2>&1; then
+ SUDO=sudo
+ SUDO_ENV="CERTBOT_AUTO=$0"
+ else
+ echo \"sudo\" is not available, will use \"su\" for installation steps...
+ SUDO=su_sudo
+ fi
+ else
+ SUDO=
+ fi
fi
ExperimentalBootstrap() {
@@ -136,7 +172,7 @@ ExperimentalBootstrap() {
DeterminePythonVersion() {
for LE_PYTHON in "$LE_PYTHON" python2.7 python27 python2 python; do
# Break (while keeping the LE_PYTHON value) if found.
- command -v "$LE_PYTHON" > /dev/null && break
+ $EXISTS "$LE_PYTHON" > /dev/null && break
done
if [ "$?" != "0" ]; then
echo "Cannot find any Pythons; please install one!"
@@ -177,19 +213,22 @@ BootstrapDebCommon() {
# distro version (#346)
virtualenv=
- if apt-cache show virtualenv > /dev/null 2>&1 && ! apt-cache --quiet=0 show virtualenv 2>&1 | grep -q 'No packages found'; then
- virtualenv="virtualenv"
+ # virtual env is known to apt and is installable
+ if apt-cache show virtualenv > /dev/null 2>&1 ; then
+ if ! LC_ALL=C apt-cache --quiet=0 show virtualenv 2>&1 | grep -q 'No packages found'; then
+ virtualenv="virtualenv"
+ fi
fi
if apt-cache show python-virtualenv > /dev/null 2>&1; then
- virtualenv="$virtualenv python-virtualenv"
+ virtualenv="$virtualenv python-virtualenv"
fi
augeas_pkg="libaugeas0 augeas-lenses"
- AUGVERSION=`apt-cache show --no-all-versions libaugeas0 | grep ^Version: | cut -d" " -f2`
+ AUGVERSION=`LC_ALL=C apt-cache show --no-all-versions libaugeas0 | grep ^Version: | cut -d" " -f2`
if [ "$ASSUME_YES" = 1 ]; then
- YES_FLAG="-y"
+ YES_FLAG="-y"
fi
AddBackportRepo() {
@@ -248,15 +287,15 @@ BootstrapDebCommon() {
python-dev \
$virtualenv \
gcc \
- dialog \
$augeas_pkg \
libssl-dev \
+ openssl \
libffi-dev \
ca-certificates \
- if ! command -v virtualenv > /dev/null ; then
+ if ! $EXISTS virtualenv > /dev/null ; then
echo Failed to install a working \"virtualenv\" command, exiting
exit 1
fi
@@ -307,7 +346,6 @@ BootstrapRpmCommon() {
pkgs="
gcc
- dialog
augeas-libs
openssl
openssl-devel
@@ -361,7 +399,6 @@ BootstrapSuseCommon() {
python-devel \
python-virtualenv \
gcc \
- dialog \
augeas-lenses \
libopenssl-devel \
libffi-devel \
@@ -380,7 +417,6 @@ BootstrapArchCommon() {
python2
python-virtualenv
gcc
- dialog
augeas
openssl
libffi
@@ -404,22 +440,26 @@ BootstrapGentooCommon() {
PACKAGES="
dev-lang/python:2.7
dev-python/virtualenv
- dev-util/dialog
app-admin/augeas
dev-libs/openssl
dev-libs/libffi
app-misc/ca-certificates
virtual/pkgconfig"
+ ASK_OPTION="--ask"
+ if [ "$ASSUME_YES" = 1 ]; then
+ ASK_OPTION=""
+ fi
+
case "$PACKAGE_MANAGER" in
(paludis)
$SUDO cave resolve --preserve-world --keep-targets if-possible $PACKAGES -x
;;
(pkgcore)
- $SUDO pmerge --noreplace --oneshot $PACKAGES
+ $SUDO pmerge --noreplace --oneshot $ASK_OPTION $PACKAGES
;;
(portage|*)
- $SUDO emerge --noreplace --oneshot $PACKAGES
+ $SUDO emerge --noreplace --oneshot $ASK_OPTION $PACKAGES
;;
esac
}
@@ -449,7 +489,6 @@ BootstrapMac() {
fi
$pkgcmd augeas
- $pkgcmd dialog
if [ "$(which python)" = "/System/Library/Frameworks/Python.framework/Versions/2.7/bin/python" \
-o "$(which python)" = "/usr/bin/python" ]; then
# We want to avoid using the system Python because it requires root to use pip.
@@ -458,7 +497,7 @@ BootstrapMac() {
$pkgcmd python
fi
- # Workaround for _dlopen not finding augeas on OS X
+ # Workaround for _dlopen not finding augeas on macOS
if [ "$pkgman" = "port" ] && ! [ -e "/usr/local/lib/libaugeas.dylib" ] && [ -e "/opt/local/lib/libaugeas.dylib" ]; then
echo "Applying augeas workaround"
$SUDO mkdir -p /usr/local/lib/
@@ -496,8 +535,8 @@ BootstrapMageiaCommon() {
if ! $SUDO urpmi --force \
git \
gcc \
- cdialog \
python-augeas \
+ openssl \
libopenssl-devel \
libffi-devel \
rootcerts
@@ -541,7 +580,7 @@ Bootstrap() {
elif uname | grep -iq FreeBSD ; then
ExperimentalBootstrap "FreeBSD" BootstrapFreeBsd
elif uname | grep -iq Darwin ; then
- ExperimentalBootstrap "Mac OS X" BootstrapMac
+ ExperimentalBootstrap "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
@@ -557,7 +596,7 @@ Bootstrap() {
}
TempDir() {
- mktemp -d 2>/dev/null || mktemp -d -t 'le' # Linux || OS X
+ mktemp -d 2>/dev/null || mktemp -d -t 'le' # Linux || macOS
}
@@ -594,6 +633,11 @@ if [ "$1" = "--le-auto-phase2" ]; then
# `pip install --no-cache-dir -e acme -e . -e certbot-apache -e certbot-nginx`,
# and then use `hashin` or a more secure method to gather the hashes.
+# Hashin example:
+# pip install hashin
+# hashin -r letsencrypt-auto-requirements.txt cryptography==1.5.2
+# sets the new certbot-auto pinned version of cryptography to 1.5.2
+
argparse==1.4.0 \
--hash=sha256:c31647edb69fd3d465a847ea3157d37bed1f95f19760b11a47aa91c04b666314 \
--hash=sha256:62b089a55be1d8949cd2bc7e0df0bddb9e028faefc8c32038cc84862aefdd6e4
@@ -601,7 +645,8 @@ argparse==1.4.0 \
# This comes before cffi because cffi will otherwise install an unchecked
# version via setup_requires.
pycparser==2.14 \
- --hash=sha256:7959b4a74abdc27b312fed1c21e6caf9309ce0b29ea86b591fd2e99ecdf27f73
+ --hash=sha256:7959b4a74abdc27b312fed1c21e6caf9309ce0b29ea86b591fd2e99ecdf27f73 \
+ --no-binary pycparser
cffi==1.4.2 \
--hash=sha256:53c1c9ddb30431513eb7f3cdef0a3e06b0f1252188aaa7744af0f5a4cd45dbaf \
@@ -624,29 +669,29 @@ ConfigArgParse==0.10.0 \
--hash=sha256:3b50a83dd58149dfcee98cb6565265d10b53e9c0a2bca7eeef7fb5f5524890a7
configobj==5.0.6 \
--hash=sha256:a2f5650770e1c87fb335af19a9b7eb73fc05ccf22144eb68db7d00cd2bcb0902
-cryptography==1.3.4 \
- --hash=sha256:bede00edd11a2a62c8c98c271cc103fa3a3d72acf64f6e5e4eaf251128897b17 \
- --hash=sha256:53b39e687b744bb548a98f40736cc529d9f60959b4e6cc551322cf9505d35eb3 \
- --hash=sha256:474b73ad1139b4e423e46bbd818efd0d5c0df1c65d9f7c957d64c9215d77afde \
- --hash=sha256:aaddf9592d5b99e32dd518bb4a25b147c124f9d6b4ad64b94f01b15d1666b8c8 \
- --hash=sha256:6dcad2f407db8c3cd6ecd78361439c449a4f94786b46c54507e7e68f51e1709d \
- --hash=sha256:475c153fc622e656f1f10a9c9941d0ac7ab18df7c38d35d563a437c1c0e34f24 \
- --hash=sha256:86dd61df581cba04e89e45081efbc531faff1c9d99c77b1ce97f87216c356353 \
- --hash=sha256:75cc697e4ef5fdd0102ca749114c6370dbd11db0c9132a18834858c2566247e3 \
- --hash=sha256:ea03ad5b9df6d79fc9fc1ab23729e01e1c920d2974c5e3c634ccf45a5c378452 \
- --hash=sha256:c8872b8fe4f3416d6338ab99612f49ab314f7856cb43bffab2a32d28a6267be8 \
- --hash=sha256:468fc6e16eaec6ceaa6bc341273e6e9912d01b42b740f8cf896ace7fcd6a321d \
- --hash=sha256:d6fea3c6502735011c5d61a62aef1c1d770fc6a2def45d9e6c0d94c9651e3317 \
- --hash=sha256:3cf95f179f4bead3d5649b91860ef4cf60ad4244209190fc405908272576d961 \
- --hash=sha256:141f77e60a5b9158309b2b60288c7f81d37faa15c22a69b94c190ceefaaa6236 \
- --hash=sha256:87b7a1fe703c6424451f3372d1879dae91c7fe5e13375441a72833db76fee30e \
- --hash=sha256:f5ee3cb0cf1a6550bf483ccffa6608db267a377b45f7e3a8201a86d1d8feb19f \
- --hash=sha256:4e097286651ea318300af3251375d48b71b8228481c56cd617ddd4459a1ff261 \
- --hash=sha256:1e3d3ae3f22f22d50d340f47f25227511326f3f1396c6d2446a5b45b516c4313 \
- --hash=sha256:6a057941cb64d79834ea3cf99093fcc4787c2a5d44f686c4f297361ddc419bcd \
- --hash=sha256:68b3d5390b92559ddd3353c73ab2dfcff758f9c4ec4f5d5226ccede0e5d779f4 \
- --hash=sha256:545dc003b4b6081f9c3e452da15d819b04b696f49484aff64c0a2aedf766bef8 \
- --hash=sha256:423ff890c01be7c70dbfeaa967eeef5146f1a43a5f810ffdc07b178e48a105a9
+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
enum34==1.1.2 \
--hash=sha256:2475d7fcddf5951e92ff546972758802de5260bf409319a9f1934e6bbc8b1dc7 \
--hash=sha256:35907defb0f992b75ab7788f65fedc1cf20ffa22688e0e6f6f12afc06b3ea501
@@ -662,8 +707,6 @@ ipaddress==1.0.16 \
linecache2==1.0.0 \
--hash=sha256:e78be9c0a0dfcbac712fe04fbf92b96cddae80b1b842f24248214c8496f006ef \
--hash=sha256:4b26ff4e7110db76eeb6f5a7b64a82623839d595c2038eeda662f2a2db78e97c
-ndg-httpsclient==0.4.0 \
- --hash=sha256:e8c155fdebd9c4bcb0810b4ed01ae1987554b1ee034dd7532d7b8fdae38a6274
ordereddict==1.1 \
--hash=sha256:1c35b4ac206cef2d24816c89f89cf289dd3d38cf7c449bb3fab7bf6d43f01b1f
parsedatetime==2.1 \
@@ -684,9 +727,9 @@ pyasn1==0.1.9 \
--hash=sha256:5191ff6b9126d2c039dd87f8ff025bed274baf07fa78afa46f556b1ad7265d6e \
--hash=sha256:8323e03637b2d072cc7041300bac6ec448c3c28950ab40376036788e9a1af629 \
--hash=sha256:853cacd96d1f701ddd67aa03ecc05f51890135b7262e922710112f12a2ed2a7f
-pyopenssl==16.0.0 \
- --hash=sha256:5add70cf00273bf957ca31fdb0df9b0ae4639e081897d5f86a0ae1f104901230 \
- --hash=sha256:363d10ee43d062285facf4e465f4f5163f9f702f9134f0a5896f134cbb92d17d
+pyOpenSSL==16.2.0 \
+ --hash=sha256:26ca380ddf272f7556e48064bbcd5bd71f83dfc144f3583501c7ddbd9434ee17 \
+ --hash=sha256:7779a3bbb74e79db234af6a08775568c6769b5821faecf6e2f4143edb227516e
pyparsing==2.1.8 \
--hash=sha256:2f0f5ceb14eccd5aef809d6382e87df22ca1da583c79f6db01675ce7d7f49c18 \
--hash=sha256:03a4869b9f3493807ee1f1cb405e6d576a1a2ca4d81a982677c0c1ad6177c56b \
@@ -701,9 +744,6 @@ pyRFC3339==1.0 \
--hash=sha256:8dfbc6c458b8daba1c0f3620a8c78008b323a268b27b7359e92a4ae41325f535
python-augeas==0.5.0 \
--hash=sha256:67d59d66cdba8d624e0389b87b2a83a176f21f16a87553b50f5703b23f29bac2
-python2-pythondialog==3.3.0 \
- --hash=sha256:04e93f24995c43dd90f338d5d865ca72ce3fb5a5358d4daa4965571db35fc3ec \
- --hash=sha256:3e6f593fead98f8a526bc3e306933533236e33729f552f52896ea504f55313fa
pytz==2015.7 \
--hash=sha256:3abe6a6d3fc2fbbe4c60144211f45da2edbe3182a6f6511af6bbba0598b1f992 \
--hash=sha256:939ef9c1e1224d980405689a97ffcf7828c56d1517b31d73464356c1f2b7769e \
@@ -718,9 +758,9 @@ pytz==2015.7 \
--hash=sha256:fbd26746772c24cb93c8b97cbdad5cb9e46c86bbdb1b9d8a743ee00e2fb1fc5d \
--hash=sha256:99266ef30a37e43932deec2b7ca73e83c8dbc3b9ff703ec73eca6b1dae6befea \
--hash=sha256:8b6ce1c993909783bc96e0b4f34ea223bff7a4df2c90bdb9c4e0f1ac928689e3
-requests==2.9.1 \
- --hash=sha256:113fbba5531a9e34945b7d36b33a084e8ba5d0664b703c81a7c572d91919a5b8 \
- --hash=sha256:c577815dd00f1394203fc44eb979724b098f88264a9ef898ee45b8e5e9cf587f
+requests==2.12.1 \
+ --hash=sha256:3f3f27a9d0f9092935efc78054ef324eb9f8166718270aefe036dfa1e4f68e1e \
+ --hash=sha256:2109ecea94df90980be040490ff1d879971b024861539abb00054062388b612e
six==1.10.0 \
--hash=sha256:0ff78c403d9bccf5a425a6d31a12aa6b47f1c21ca4dc2573a7e2f32a97335eb1 \
--hash=sha256:105f8d68616f8248e24bf0e9372ef04d3cc10104f1980f54d57b2ce73a5ad56a
@@ -761,18 +801,18 @@ letsencrypt==0.7.0 \
# THE LINES BELOW ARE EDITED BY THE RELEASE SCRIPT; ADD ALL DEPENDENCIES ABOVE.
-acme==0.9.3 \
- --hash=sha256:d18ce17a75ad24d27981dfaef0524aa905eab757b267e027162b56a8967ab8fb \
- --hash=sha256:a6eff1f955eb2e4316abd9aa2fedb6d9345e6b5b8a2d64ea0ad35e05d6124099
-certbot==0.9.3 \
- --hash=sha256:a87ef4c53c018df4e52ee2f2e906ad16bbb37789f29e6f284c495a2eb4d9b243 \
- --hash=sha256:68149cb8392b29f5d5246e7226d25f913f2b10482bf3bc7368e8c8821d25f3b0
-certbot-apache==0.9.3 \
- --hash=sha256:f379b1053e10709692654d7a6fcea9eaed19b66c49a753b61e31bd06a04b0aac \
- --hash=sha256:a5d98cf972072de08f984db4e6a7f20269f3f023c43f6d4e781fe43be7c10086
-certbot-nginx==0.9.3 \
- --hash=sha256:3c26f18f0b57550f069263bd9b2984ef33eab6693e7796611c1b2cc16574069c \
- --hash=sha256:7337a2e90e0b28a1ab09e31d9fb81c6d78e6453500c824c0f18bab5d31b63058
+acme==0.10.0 \
+ --hash=sha256:df4299a9881d94185a1578ed97334430a90f761ce815edd300860ca47d0538f1 \
+ --hash=sha256:ddebdf1fe139c8fedbcf633955ec867496d2f7d2d2e9879d538437a69ab47876
+certbot==0.10.0 \
+ --hash=sha256:fb1bfa3d54ce9366758e374f7ed99667ce20484224934d3e8e57839fcf784bc5 \
+ --hash=sha256:dd64ed8fb3cc3b053f05e779b934433445918668c49bcdbb2c816062815e1661
+certbot-apache==0.10.0 \
+ --hash=sha256:909d59c53507093f838f7336f75d7d78563a35b16afdf6c30f45c9f47bf069da \
+ --hash=sha256:6f110dae227dd0fea9572fa12dd60b041e391f5d2028cc2e1fedd2a9a0d2bc88
+certbot-nginx==0.10.0 \
+ --hash=sha256:4f33a230d420cbd0431e7b707fb9a1732bfd18d3c6056019591bd7c3a13abe92 \
+ --hash=sha256:c12ffd05207b0be3c765b3d3e2927e0b2cc2b7de20654b19d154a0d789e7c1d5
UNLIKELY_EOF
# -------------------------------------------------------------------------
@@ -940,7 +980,28 @@ UNLIKELY_EOF
# Report error. (Otherwise, be quiet.)
echo "Had a problem while installing Python packages."
if [ "$VERBOSE" != 1 ]; then
+ echo
+ echo "pip prints the following errors: "
+ echo "====================================================="
echo "$PIP_OUT"
+ echo "====================================================="
+ echo
+ echo "Certbot has problem setting up the virtual environment."
+
+ if `echo $PIP_OUT | grep -q Killed` || `echo $PIP_OUT | grep -q "allocate memory"` ; then
+ echo
+ echo "Based on your pip output, the problem can likely be fixed by "
+ echo "increasing the available memory."
+ else
+ echo
+ echo "We were not be able to guess the right solution from your pip "
+ echo "output."
+ fi
+
+ echo
+ echo "Consult https://certbot.eff.org/docs/install.html#problems-with-python-virtual-environment"
+ echo "for possible solutions."
+ echo "You may also find some support resources at https://certbot.eff.org/support/ ."
fi
rm -rf "$VENV_PATH"
exit 1
@@ -1132,7 +1193,7 @@ UNLIKELY_EOF
# TODO: Deal with quotes in pathnames.
echo "Replacing certbot-auto..."
# Clone permissions with cp. chmod and chown don't have a --reference
- # option on OS X or BSD, and stat -c on Linux is stat -f on OS X and BSD:
+ # option on macOS or BSD, and stat -c on Linux is stat -f on macOS and BSD:
$SUDO cp -p "$0" "$TEMP_DIR/letsencrypt-auto.permission-clone"
$SUDO cp "$TEMP_DIR/letsencrypt-auto" "$TEMP_DIR/letsencrypt-auto.permission-clone"
# Using mv rather than cp leaves the old file descriptor pointing to the
diff --git a/letsencrypt-auto-source/letsencrypt-auto.sig b/letsencrypt-auto-source/letsencrypt-auto.sig
index f3950b7d6..1657814ae 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 991d9dd76..b602540a0 100755
--- a/letsencrypt-auto-source/letsencrypt-auto.template
+++ b/letsencrypt-auto-source/letsencrypt-auto.template
@@ -15,9 +15,13 @@ set -e # Work even if somebody does "sh thisscript.sh".
# Note: you can set XDG_DATA_HOME or VENV_PATH before running this script,
# if you want to change where the virtual environment will be installed
-XDG_DATA_HOME=${XDG_DATA_HOME:-~/.local/share}
+if [ -z "$XDG_DATA_HOME" ]; then
+ XDG_DATA_HOME=~/.local/share
+fi
VENV_NAME="letsencrypt"
-VENV_PATH=${VENV_PATH:-"$XDG_DATA_HOME/$VENV_NAME"}
+if [ -z "$VENV_PATH" ]; then
+ VENV_PATH="$XDG_DATA_HOME/$VENV_NAME"
+fi
VENV_BIN="$VENV_PATH/bin"
LE_AUTO_VERSION="{{ LE_AUTO_VERSION }}"
BASENAME=$(basename $0)
@@ -58,6 +62,7 @@ for arg in "$@" ; do
--verbose)
VERBOSE=1;;
-[!-]*)
+ OPTIND=1
while getopts ":hnvq" short_arg $arg; do
case "$short_arg" in
h)
@@ -79,43 +84,74 @@ if [ $BASENAME = "letsencrypt-auto" ]; then
HELP=0
fi
+# Support for busybox and others where there is no "command",
+# but "which" instead
+if command -v command > /dev/null 2>&1 ; then
+ export EXISTS="command -v"
+elif which which > /dev/null 2>&1 ; then
+ export EXISTS="which"
+else
+ echo "Cannot find command nor which... please install one!"
+ exit 1
+fi
+
# certbot-auto needs root access to bootstrap OS dependencies, and
# certbot itself needs root access for almost all modes of operation
# The "normal" case is that sudo is used for the steps that need root, but
# this script *can* be run as root (not recommended), or fall back to using
-# `su`
+# `su`. Auto-detection can be overrided by explicitly setting the
+# environment variable LE_AUTO_SUDO to 'sudo', 'sudo_su' or '' as used below.
+
+# Because the parameters in `su -c` has to be a string,
+# we need to properly escape it.
+su_sudo() {
+ args=""
+ # This `while` loop iterates over all parameters given to this function.
+ # For each parameter, all `'` will be replace by `'"'"'`, and the escaped string
+ # will be wrapped in a pair of `'`, then appended to `$args` string
+ # For example, `echo "It's only 1\$\!"` will be escaped to:
+ # 'echo' 'It'"'"'s only 1$!'
+ # │ │└┼┘│
+ # │ │ │ └── `'s only 1$!'` the literal string
+ # │ │ └── `\"'\"` is a single quote (as a string)
+ # │ └── `'It'`, to be concatenated with the strings following it
+ # └── `echo` wrapped in a pair of `'`, it's totally fine for the shell command itself
+ while [ $# -ne 0 ]; do
+ args="$args'$(printf "%s" "$1" | sed -e "s/'/'\"'\"'/g")' "
+ shift
+ done
+ su root -c "$args"
+}
+
SUDO_ENV=""
export CERTBOT_AUTO="$0"
-if test "`id -u`" -ne "0" ; then
- if command -v sudo 1>/dev/null 2>&1; then
- SUDO=sudo
- SUDO_ENV="CERTBOT_AUTO=$0"
- else
- echo \"sudo\" is not available, will use \"su\" for installation steps...
- # Because the parameters in `su -c` has to be a string,
- # we need properly escape it
- su_sudo() {
- args=""
- # This `while` loop iterates over all parameters given to this function.
- # For each parameter, all `'` will be replace by `'"'"'`, and the escaped string
- # will be wrapped in a pair of `'`, then appended to `$args` string
- # For example, `echo "It's only 1\$\!"` will be escaped to:
- # 'echo' 'It'"'"'s only 1$!'
- # │ │└┼┘│
- # │ │ │ └── `'s only 1$!'` the literal string
- # │ │ └── `\"'\"` is a single quote (as a string)
- # │ └── `'It'`, to be concatenated with the strings following it
- # └── `echo` wrapped in a pair of `'`, it's totally fine for the shell command itself
- while [ $# -ne 0 ]; do
- args="$args'$(printf "%s" "$1" | sed -e "s/'/'\"'\"'/g")' "
- shift
- done
- su root -c "$args"
- }
- SUDO=su_sudo
- fi
+if [ -n "${LE_AUTO_SUDO+x}" ]; then
+ case "$LE_AUTO_SUDO" in
+ su_sudo|su)
+ SUDO=su_sudo
+ ;;
+ sudo)
+ SUDO=sudo
+ SUDO_ENV="CERTBOT_AUTO=$0"
+ ;;
+ '') ;; # Nothing to do for plain root method.
+ *)
+ echo "Error: unknown root authorization mechanism '$LE_AUTO_SUDO'."
+ exit 1
+ esac
+ echo "Using preset root authorization mechanism '$LE_AUTO_SUDO'."
else
- SUDO=
+ if test "`id -u`" -ne "0" ; then
+ if $EXISTS sudo 1>/dev/null 2>&1; then
+ SUDO=sudo
+ SUDO_ENV="CERTBOT_AUTO=$0"
+ else
+ echo \"sudo\" is not available, will use \"su\" for installation steps...
+ SUDO=su_sudo
+ fi
+ else
+ SUDO=
+ fi
fi
ExperimentalBootstrap() {
@@ -136,7 +172,7 @@ ExperimentalBootstrap() {
DeterminePythonVersion() {
for LE_PYTHON in "$LE_PYTHON" python2.7 python27 python2 python; do
# Break (while keeping the LE_PYTHON value) if found.
- command -v "$LE_PYTHON" > /dev/null && break
+ $EXISTS "$LE_PYTHON" > /dev/null && break
done
if [ "$?" != "0" ]; then
echo "Cannot find any Pythons; please install one!"
@@ -195,7 +231,7 @@ Bootstrap() {
elif uname | grep -iq FreeBSD ; then
ExperimentalBootstrap "FreeBSD" BootstrapFreeBsd
elif uname | grep -iq Darwin ; then
- ExperimentalBootstrap "Mac OS X" BootstrapMac
+ ExperimentalBootstrap "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
@@ -211,7 +247,7 @@ Bootstrap() {
}
TempDir() {
- mktemp -d 2>/dev/null || mktemp -d -t 'le' # Linux || OS X
+ mktemp -d 2>/dev/null || mktemp -d -t 'le' # Linux || macOS
}
@@ -264,7 +300,28 @@ UNLIKELY_EOF
# Report error. (Otherwise, be quiet.)
echo "Had a problem while installing Python packages."
if [ "$VERBOSE" != 1 ]; then
+ echo
+ echo "pip prints the following errors: "
+ echo "====================================================="
echo "$PIP_OUT"
+ echo "====================================================="
+ echo
+ echo "Certbot has problem setting up the virtual environment."
+
+ if `echo $PIP_OUT | grep -q Killed` || `echo $PIP_OUT | grep -q "allocate memory"` ; then
+ echo
+ echo "Based on your pip output, the problem can likely be fixed by "
+ echo "increasing the available memory."
+ else
+ echo
+ echo "We were not be able to guess the right solution from your pip "
+ echo "output."
+ fi
+
+ echo
+ echo "Consult https://certbot.eff.org/docs/install.html#problems-with-python-virtual-environment"
+ echo "for possible solutions."
+ echo "You may also find some support resources at https://certbot.eff.org/support/ ."
fi
rm -rf "$VENV_PATH"
exit 1
@@ -330,7 +387,7 @@ UNLIKELY_EOF
# TODO: Deal with quotes in pathnames.
echo "Replacing certbot-auto..."
# Clone permissions with cp. chmod and chown don't have a --reference
- # option on OS X or BSD, and stat -c on Linux is stat -f on OS X and BSD:
+ # option on macOS or BSD, and stat -c on Linux is stat -f on macOS and BSD:
$SUDO cp -p "$0" "$TEMP_DIR/letsencrypt-auto.permission-clone"
$SUDO cp "$TEMP_DIR/letsencrypt-auto" "$TEMP_DIR/letsencrypt-auto.permission-clone"
# Using mv rather than cp leaves the old file descriptor pointing to the
diff --git a/letsencrypt-auto-source/pieces/bootstrappers/arch_common.sh b/letsencrypt-auto-source/pieces/bootstrappers/arch_common.sh
index 39e2da5fe..333f56ff7 100755
--- a/letsencrypt-auto-source/pieces/bootstrappers/arch_common.sh
+++ b/letsencrypt-auto-source/pieces/bootstrappers/arch_common.sh
@@ -10,7 +10,6 @@ BootstrapArchCommon() {
python2
python-virtualenv
gcc
- dialog
augeas
openssl
libffi
diff --git a/letsencrypt-auto-source/pieces/bootstrappers/deb_common.sh b/letsencrypt-auto-source/pieces/bootstrappers/deb_common.sh
index 8eb7e16ee..ec60b7525 100644
--- a/letsencrypt-auto-source/pieces/bootstrappers/deb_common.sh
+++ b/letsencrypt-auto-source/pieces/bootstrappers/deb_common.sh
@@ -23,19 +23,22 @@ BootstrapDebCommon() {
# distro version (#346)
virtualenv=
- if apt-cache show virtualenv > /dev/null 2>&1 && ! apt-cache --quiet=0 show virtualenv 2>&1 | grep -q 'No packages found'; then
- virtualenv="virtualenv"
+ # virtual env is known to apt and is installable
+ if apt-cache show virtualenv > /dev/null 2>&1 ; then
+ if ! LC_ALL=C apt-cache --quiet=0 show virtualenv 2>&1 | grep -q 'No packages found'; then
+ virtualenv="virtualenv"
+ fi
fi
if apt-cache show python-virtualenv > /dev/null 2>&1; then
- virtualenv="$virtualenv python-virtualenv"
+ virtualenv="$virtualenv python-virtualenv"
fi
augeas_pkg="libaugeas0 augeas-lenses"
- AUGVERSION=`apt-cache show --no-all-versions libaugeas0 | grep ^Version: | cut -d" " -f2`
+ AUGVERSION=`LC_ALL=C apt-cache show --no-all-versions libaugeas0 | grep ^Version: | cut -d" " -f2`
if [ "$ASSUME_YES" = 1 ]; then
- YES_FLAG="-y"
+ YES_FLAG="-y"
fi
AddBackportRepo() {
@@ -94,15 +97,15 @@ BootstrapDebCommon() {
python-dev \
$virtualenv \
gcc \
- dialog \
$augeas_pkg \
libssl-dev \
+ openssl \
libffi-dev \
ca-certificates \
- if ! command -v virtualenv > /dev/null ; then
+ if ! $EXISTS virtualenv > /dev/null ; then
echo Failed to install a working \"virtualenv\" command, exiting
exit 1
fi
diff --git a/letsencrypt-auto-source/pieces/bootstrappers/gentoo_common.sh b/letsencrypt-auto-source/pieces/bootstrappers/gentoo_common.sh
index 580b69a0d..86a1ec7d6 100755
--- a/letsencrypt-auto-source/pieces/bootstrappers/gentoo_common.sh
+++ b/letsencrypt-auto-source/pieces/bootstrappers/gentoo_common.sh
@@ -2,22 +2,26 @@ BootstrapGentooCommon() {
PACKAGES="
dev-lang/python:2.7
dev-python/virtualenv
- dev-util/dialog
app-admin/augeas
dev-libs/openssl
dev-libs/libffi
app-misc/ca-certificates
virtual/pkgconfig"
+ ASK_OPTION="--ask"
+ if [ "$ASSUME_YES" = 1 ]; then
+ ASK_OPTION=""
+ fi
+
case "$PACKAGE_MANAGER" in
(paludis)
$SUDO cave resolve --preserve-world --keep-targets if-possible $PACKAGES -x
;;
(pkgcore)
- $SUDO pmerge --noreplace --oneshot $PACKAGES
+ $SUDO pmerge --noreplace --oneshot $ASK_OPTION $PACKAGES
;;
(portage|*)
- $SUDO emerge --noreplace --oneshot $PACKAGES
+ $SUDO emerge --noreplace --oneshot $ASK_OPTION $PACKAGES
;;
esac
}
diff --git a/letsencrypt-auto-source/pieces/bootstrappers/mac.sh b/letsencrypt-auto-source/pieces/bootstrappers/mac.sh
index 2b04977c8..cafce037a 100755
--- a/letsencrypt-auto-source/pieces/bootstrappers/mac.sh
+++ b/letsencrypt-auto-source/pieces/bootstrappers/mac.sh
@@ -15,7 +15,6 @@ BootstrapMac() {
fi
$pkgcmd augeas
- $pkgcmd dialog
if [ "$(which python)" = "/System/Library/Frameworks/Python.framework/Versions/2.7/bin/python" \
-o "$(which python)" = "/usr/bin/python" ]; then
# We want to avoid using the system Python because it requires root to use pip.
@@ -24,7 +23,7 @@ BootstrapMac() {
$pkgcmd python
fi
- # Workaround for _dlopen not finding augeas on OS X
+ # Workaround for _dlopen not finding augeas on macOS
if [ "$pkgman" = "port" ] && ! [ -e "/usr/local/lib/libaugeas.dylib" ] && [ -e "/opt/local/lib/libaugeas.dylib" ]; then
echo "Applying augeas workaround"
$SUDO mkdir -p /usr/local/lib/
diff --git a/letsencrypt-auto-source/pieces/bootstrappers/mageia_common.sh b/letsencrypt-auto-source/pieces/bootstrappers/mageia_common.sh
index d6651574a..fb417fd17 100644
--- a/letsencrypt-auto-source/pieces/bootstrappers/mageia_common.sh
+++ b/letsencrypt-auto-source/pieces/bootstrappers/mageia_common.sh
@@ -11,8 +11,8 @@ BootstrapMageiaCommon() {
if ! $SUDO urpmi --force \
git \
gcc \
- cdialog \
python-augeas \
+ openssl \
libopenssl-devel \
libffi-devel \
rootcerts
diff --git a/letsencrypt-auto-source/pieces/bootstrappers/rpm_common.sh b/letsencrypt-auto-source/pieces/bootstrappers/rpm_common.sh
index 2fd629ff8..26d717ea1 100755
--- a/letsencrypt-auto-source/pieces/bootstrappers/rpm_common.sh
+++ b/letsencrypt-auto-source/pieces/bootstrappers/rpm_common.sh
@@ -43,7 +43,6 @@ BootstrapRpmCommon() {
pkgs="
gcc
- dialog
augeas-libs
openssl
openssl-devel
diff --git a/letsencrypt-auto-source/pieces/bootstrappers/suse_common.sh b/letsencrypt-auto-source/pieces/bootstrappers/suse_common.sh
index 9ac295922..bd4d9c68d 100755
--- a/letsencrypt-auto-source/pieces/bootstrappers/suse_common.sh
+++ b/letsencrypt-auto-source/pieces/bootstrappers/suse_common.sh
@@ -11,7 +11,6 @@ BootstrapSuseCommon() {
python-devel \
python-virtualenv \
gcc \
- dialog \
augeas-lenses \
libopenssl-devel \
libffi-devel \
diff --git a/letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt b/letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt
index 1803d51b8..59769df85 100644
--- a/letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt
+++ b/letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt
@@ -3,6 +3,11 @@
# `pip install --no-cache-dir -e acme -e . -e certbot-apache -e certbot-nginx`,
# and then use `hashin` or a more secure method to gather the hashes.
+# Hashin example:
+# pip install hashin
+# hashin -r letsencrypt-auto-requirements.txt cryptography==1.5.2
+# sets the new certbot-auto pinned version of cryptography to 1.5.2
+
argparse==1.4.0 \
--hash=sha256:c31647edb69fd3d465a847ea3157d37bed1f95f19760b11a47aa91c04b666314 \
--hash=sha256:62b089a55be1d8949cd2bc7e0df0bddb9e028faefc8c32038cc84862aefdd6e4
@@ -10,7 +15,8 @@ argparse==1.4.0 \
# This comes before cffi because cffi will otherwise install an unchecked
# version via setup_requires.
pycparser==2.14 \
- --hash=sha256:7959b4a74abdc27b312fed1c21e6caf9309ce0b29ea86b591fd2e99ecdf27f73
+ --hash=sha256:7959b4a74abdc27b312fed1c21e6caf9309ce0b29ea86b591fd2e99ecdf27f73 \
+ --no-binary pycparser
cffi==1.4.2 \
--hash=sha256:53c1c9ddb30431513eb7f3cdef0a3e06b0f1252188aaa7744af0f5a4cd45dbaf \
@@ -33,29 +39,29 @@ ConfigArgParse==0.10.0 \
--hash=sha256:3b50a83dd58149dfcee98cb6565265d10b53e9c0a2bca7eeef7fb5f5524890a7
configobj==5.0.6 \
--hash=sha256:a2f5650770e1c87fb335af19a9b7eb73fc05ccf22144eb68db7d00cd2bcb0902
-cryptography==1.3.4 \
- --hash=sha256:bede00edd11a2a62c8c98c271cc103fa3a3d72acf64f6e5e4eaf251128897b17 \
- --hash=sha256:53b39e687b744bb548a98f40736cc529d9f60959b4e6cc551322cf9505d35eb3 \
- --hash=sha256:474b73ad1139b4e423e46bbd818efd0d5c0df1c65d9f7c957d64c9215d77afde \
- --hash=sha256:aaddf9592d5b99e32dd518bb4a25b147c124f9d6b4ad64b94f01b15d1666b8c8 \
- --hash=sha256:6dcad2f407db8c3cd6ecd78361439c449a4f94786b46c54507e7e68f51e1709d \
- --hash=sha256:475c153fc622e656f1f10a9c9941d0ac7ab18df7c38d35d563a437c1c0e34f24 \
- --hash=sha256:86dd61df581cba04e89e45081efbc531faff1c9d99c77b1ce97f87216c356353 \
- --hash=sha256:75cc697e4ef5fdd0102ca749114c6370dbd11db0c9132a18834858c2566247e3 \
- --hash=sha256:ea03ad5b9df6d79fc9fc1ab23729e01e1c920d2974c5e3c634ccf45a5c378452 \
- --hash=sha256:c8872b8fe4f3416d6338ab99612f49ab314f7856cb43bffab2a32d28a6267be8 \
- --hash=sha256:468fc6e16eaec6ceaa6bc341273e6e9912d01b42b740f8cf896ace7fcd6a321d \
- --hash=sha256:d6fea3c6502735011c5d61a62aef1c1d770fc6a2def45d9e6c0d94c9651e3317 \
- --hash=sha256:3cf95f179f4bead3d5649b91860ef4cf60ad4244209190fc405908272576d961 \
- --hash=sha256:141f77e60a5b9158309b2b60288c7f81d37faa15c22a69b94c190ceefaaa6236 \
- --hash=sha256:87b7a1fe703c6424451f3372d1879dae91c7fe5e13375441a72833db76fee30e \
- --hash=sha256:f5ee3cb0cf1a6550bf483ccffa6608db267a377b45f7e3a8201a86d1d8feb19f \
- --hash=sha256:4e097286651ea318300af3251375d48b71b8228481c56cd617ddd4459a1ff261 \
- --hash=sha256:1e3d3ae3f22f22d50d340f47f25227511326f3f1396c6d2446a5b45b516c4313 \
- --hash=sha256:6a057941cb64d79834ea3cf99093fcc4787c2a5d44f686c4f297361ddc419bcd \
- --hash=sha256:68b3d5390b92559ddd3353c73ab2dfcff758f9c4ec4f5d5226ccede0e5d779f4 \
- --hash=sha256:545dc003b4b6081f9c3e452da15d819b04b696f49484aff64c0a2aedf766bef8 \
- --hash=sha256:423ff890c01be7c70dbfeaa967eeef5146f1a43a5f810ffdc07b178e48a105a9
+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
enum34==1.1.2 \
--hash=sha256:2475d7fcddf5951e92ff546972758802de5260bf409319a9f1934e6bbc8b1dc7 \
--hash=sha256:35907defb0f992b75ab7788f65fedc1cf20ffa22688e0e6f6f12afc06b3ea501
@@ -71,8 +77,6 @@ ipaddress==1.0.16 \
linecache2==1.0.0 \
--hash=sha256:e78be9c0a0dfcbac712fe04fbf92b96cddae80b1b842f24248214c8496f006ef \
--hash=sha256:4b26ff4e7110db76eeb6f5a7b64a82623839d595c2038eeda662f2a2db78e97c
-ndg-httpsclient==0.4.0 \
- --hash=sha256:e8c155fdebd9c4bcb0810b4ed01ae1987554b1ee034dd7532d7b8fdae38a6274
ordereddict==1.1 \
--hash=sha256:1c35b4ac206cef2d24816c89f89cf289dd3d38cf7c449bb3fab7bf6d43f01b1f
parsedatetime==2.1 \
@@ -93,9 +97,9 @@ pyasn1==0.1.9 \
--hash=sha256:5191ff6b9126d2c039dd87f8ff025bed274baf07fa78afa46f556b1ad7265d6e \
--hash=sha256:8323e03637b2d072cc7041300bac6ec448c3c28950ab40376036788e9a1af629 \
--hash=sha256:853cacd96d1f701ddd67aa03ecc05f51890135b7262e922710112f12a2ed2a7f
-pyopenssl==16.0.0 \
- --hash=sha256:5add70cf00273bf957ca31fdb0df9b0ae4639e081897d5f86a0ae1f104901230 \
- --hash=sha256:363d10ee43d062285facf4e465f4f5163f9f702f9134f0a5896f134cbb92d17d
+pyOpenSSL==16.2.0 \
+ --hash=sha256:26ca380ddf272f7556e48064bbcd5bd71f83dfc144f3583501c7ddbd9434ee17 \
+ --hash=sha256:7779a3bbb74e79db234af6a08775568c6769b5821faecf6e2f4143edb227516e
pyparsing==2.1.8 \
--hash=sha256:2f0f5ceb14eccd5aef809d6382e87df22ca1da583c79f6db01675ce7d7f49c18 \
--hash=sha256:03a4869b9f3493807ee1f1cb405e6d576a1a2ca4d81a982677c0c1ad6177c56b \
@@ -110,9 +114,6 @@ pyRFC3339==1.0 \
--hash=sha256:8dfbc6c458b8daba1c0f3620a8c78008b323a268b27b7359e92a4ae41325f535
python-augeas==0.5.0 \
--hash=sha256:67d59d66cdba8d624e0389b87b2a83a176f21f16a87553b50f5703b23f29bac2
-python2-pythondialog==3.3.0 \
- --hash=sha256:04e93f24995c43dd90f338d5d865ca72ce3fb5a5358d4daa4965571db35fc3ec \
- --hash=sha256:3e6f593fead98f8a526bc3e306933533236e33729f552f52896ea504f55313fa
pytz==2015.7 \
--hash=sha256:3abe6a6d3fc2fbbe4c60144211f45da2edbe3182a6f6511af6bbba0598b1f992 \
--hash=sha256:939ef9c1e1224d980405689a97ffcf7828c56d1517b31d73464356c1f2b7769e \
@@ -127,9 +128,9 @@ pytz==2015.7 \
--hash=sha256:fbd26746772c24cb93c8b97cbdad5cb9e46c86bbdb1b9d8a743ee00e2fb1fc5d \
--hash=sha256:99266ef30a37e43932deec2b7ca73e83c8dbc3b9ff703ec73eca6b1dae6befea \
--hash=sha256:8b6ce1c993909783bc96e0b4f34ea223bff7a4df2c90bdb9c4e0f1ac928689e3
-requests==2.9.1 \
- --hash=sha256:113fbba5531a9e34945b7d36b33a084e8ba5d0664b703c81a7c572d91919a5b8 \
- --hash=sha256:c577815dd00f1394203fc44eb979724b098f88264a9ef898ee45b8e5e9cf587f
+requests==2.12.1 \
+ --hash=sha256:3f3f27a9d0f9092935efc78054ef324eb9f8166718270aefe036dfa1e4f68e1e \
+ --hash=sha256:2109ecea94df90980be040490ff1d879971b024861539abb00054062388b612e
six==1.10.0 \
--hash=sha256:0ff78c403d9bccf5a425a6d31a12aa6b47f1c21ca4dc2573a7e2f32a97335eb1 \
--hash=sha256:105f8d68616f8248e24bf0e9372ef04d3cc10104f1980f54d57b2ce73a5ad56a
@@ -170,15 +171,15 @@ letsencrypt==0.7.0 \
# THE LINES BELOW ARE EDITED BY THE RELEASE SCRIPT; ADD ALL DEPENDENCIES ABOVE.
-acme==0.9.3 \
- --hash=sha256:d18ce17a75ad24d27981dfaef0524aa905eab757b267e027162b56a8967ab8fb \
- --hash=sha256:a6eff1f955eb2e4316abd9aa2fedb6d9345e6b5b8a2d64ea0ad35e05d6124099
-certbot==0.9.3 \
- --hash=sha256:a87ef4c53c018df4e52ee2f2e906ad16bbb37789f29e6f284c495a2eb4d9b243 \
- --hash=sha256:68149cb8392b29f5d5246e7226d25f913f2b10482bf3bc7368e8c8821d25f3b0
-certbot-apache==0.9.3 \
- --hash=sha256:f379b1053e10709692654d7a6fcea9eaed19b66c49a753b61e31bd06a04b0aac \
- --hash=sha256:a5d98cf972072de08f984db4e6a7f20269f3f023c43f6d4e781fe43be7c10086
-certbot-nginx==0.9.3 \
- --hash=sha256:3c26f18f0b57550f069263bd9b2984ef33eab6693e7796611c1b2cc16574069c \
- --hash=sha256:7337a2e90e0b28a1ab09e31d9fb81c6d78e6453500c824c0f18bab5d31b63058
+acme==0.10.0 \
+ --hash=sha256:df4299a9881d94185a1578ed97334430a90f761ce815edd300860ca47d0538f1 \
+ --hash=sha256:ddebdf1fe139c8fedbcf633955ec867496d2f7d2d2e9879d538437a69ab47876
+certbot==0.10.0 \
+ --hash=sha256:fb1bfa3d54ce9366758e374f7ed99667ce20484224934d3e8e57839fcf784bc5 \
+ --hash=sha256:dd64ed8fb3cc3b053f05e779b934433445918668c49bcdbb2c816062815e1661
+certbot-apache==0.10.0 \
+ --hash=sha256:909d59c53507093f838f7336f75d7d78563a35b16afdf6c30f45c9f47bf069da \
+ --hash=sha256:6f110dae227dd0fea9572fa12dd60b041e391f5d2028cc2e1fedd2a9a0d2bc88
+certbot-nginx==0.10.0 \
+ --hash=sha256:4f33a230d420cbd0431e7b707fb9a1732bfd18d3c6056019591bd7c3a13abe92 \
+ --hash=sha256:c12ffd05207b0be3c765b3d3e2927e0b2cc2b7de20654b19d154a0d789e7c1d5
diff --git a/letsencrypt-auto-source/tests/auto_test.py b/letsencrypt-auto-source/tests/auto_test.py
index 56023bc6f..6f21c28d5 100644
--- a/letsencrypt-auto-source/tests/auto_test.py
+++ b/letsencrypt-auto-source/tests/auto_test.py
@@ -279,8 +279,8 @@ class AutoTests(TestCase):
ok_(re.match(r'letsencrypt \d+\.\d+\.\d+',
err.strip().splitlines()[-1]))
# Make a few assertions to test the validity of the next tests:
- self.assertIn('Upgrading certbot-auto ', out)
- self.assertIn('Creating virtual environment...', out)
+ self.assertTrue('Upgrading certbot-auto ' in out)
+ self.assertTrue('Creating virtual environment...' in out)
# Now we have le-auto 99.9.9 and LE 99.9.9 installed. This
# conveniently sets us up to test the next 2 cases.
@@ -288,15 +288,15 @@ class AutoTests(TestCase):
# Test when neither phase-1 upgrade nor phase-2 upgrade is
# needed (probably a common case):
out, err = run_letsencrypt_auto()
- self.assertNotIn('Upgrading certbot-auto ', out)
- self.assertNotIn('Creating virtual environment...', out)
+ self.assertFalse('Upgrading certbot-auto ' in out)
+ self.assertFalse('Creating virtual environment...' in out)
# Test when a phase-1 upgrade is not needed but a phase-2
# upgrade is:
set_le_script_version(venv_dir, '0.0.1')
out, err = run_letsencrypt_auto()
- self.assertNotIn('Upgrading certbot-auto ', out)
- self.assertIn('Creating virtual environment...', out)
+ self.assertFalse('Upgrading certbot-auto ' in out)
+ self.assertTrue('Creating virtual environment...' in out)
def test_openssl_failure(self):
"""Make sure we stop if the openssl signature check fails."""
@@ -313,9 +313,8 @@ class AutoTests(TestCase):
out, err = run_le_auto(venv_dir, base_url)
except CalledProcessError as exc:
eq_(exc.returncode, 1)
- self.assertIn("Couldn't verify signature of downloaded "
- "certbot-auto.",
- exc.output)
+ self.assertTrue("Couldn't verify signature of downloaded "
+ "certbot-auto." in exc.output)
else:
self.fail('Signature check on certbot-auto erroneously passed.')
@@ -335,9 +334,8 @@ class AutoTests(TestCase):
out, err = run_le_auto(venv_dir, base_url)
except CalledProcessError as exc:
eq_(exc.returncode, 1)
- self.assertIn("THESE PACKAGES DO NOT MATCH THE HASHES "
- "FROM THE REQUIREMENTS FILE",
- exc.output)
+ self.assertTrue("THESE PACKAGES DO NOT MATCH THE HASHES "
+ "FROM THE REQUIREMENTS FILE" in exc.output)
ok_(not exists(join(venv_dir, 'letsencrypt')),
msg="The virtualenv was left around, even though "
"installation didn't succeed. We shouldn't do "
diff --git a/letsencrypt-nginx/LICENSE.txt b/letsencrypt-nginx/LICENSE.txt
deleted file mode 100644
index 02a1459be..000000000
--- a/letsencrypt-nginx/LICENSE.txt
+++ /dev/null
@@ -1,216 +0,0 @@
- 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.
-
- Incorporating code from nginxparser
- Copyright 2014 Fatih Erikli
- Licensed MIT
-
-
-Text of Apache 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
-
-Text of MIT License
-===================
-Permission is hereby granted, free of charge, to any person obtaining a copy of
-this software and associated documentation files (the "Software"), to deal in
-the Software without restriction, including without limitation the rights to
-use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
-the Software, and to permit persons to whom the Software is furnished to do so,
-subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
-FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
-COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
-IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
-CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/letsencrypt-nginx/MANIFEST.in b/letsencrypt-nginx/MANIFEST.in
deleted file mode 100644
index 97e2ad3df..000000000
--- a/letsencrypt-nginx/MANIFEST.in
+++ /dev/null
@@ -1,2 +0,0 @@
-include LICENSE.txt
-include README.rst
diff --git a/letsencrypt-nginx/README.rst b/letsencrypt-nginx/README.rst
deleted file mode 100644
index cd1f32fb8..000000000
--- a/letsencrypt-nginx/README.rst
+++ /dev/null
@@ -1,2 +0,0 @@
-This package is a simple shim for backwards compatibility around
-``certbot-nginx``, the Nginx plugin for ``certbot``.
diff --git a/letsencrypt-nginx/letsencrypt_nginx/__init__.py b/letsencrypt-nginx/letsencrypt_nginx/__init__.py
deleted file mode 100644
index aa14fe963..000000000
--- a/letsencrypt-nginx/letsencrypt_nginx/__init__.py
+++ /dev/null
@@ -1,8 +0,0 @@
-"""Let's Encrypt Nginx plugin."""
-import sys
-
-
-import certbot_nginx
-
-
-sys.modules['letsencrypt_nginx'] = certbot_nginx
diff --git a/letsencrypt-nginx/setup.py b/letsencrypt-nginx/setup.py
deleted file mode 100644
index 25db12a47..000000000
--- a/letsencrypt-nginx/setup.py
+++ /dev/null
@@ -1,59 +0,0 @@
-import codecs
-import os
-import sys
-
-from setuptools import setup
-from setuptools import find_packages
-
-
-def read_file(filename, encoding='utf8'):
- """Read unicode from given file."""
- with codecs.open(filename, encoding=encoding) as fd:
- return fd.read()
-
-
-here = os.path.abspath(os.path.dirname(__file__))
-readme = read_file(os.path.join(here, 'README.rst'))
-
-
-version = '0.8.0.dev0'
-
-
-# This package is a simple shim around certbot-nginx
-install_requires = [
- 'certbot-nginx',
- 'letsencrypt=={0}'.format(version),
-]
-
-
-setup(
- name='letsencrypt-nginx',
- version=version,
- description="Nginx plugin for Let's Encrypt",
- long_description=readme,
- url='https://github.com/letsencrypt/letsencrypt',
- 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',
- '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,
-)
diff --git a/letsencrypt/LICENSE.txt b/letsencrypt/LICENSE.txt
deleted file mode 100644
index 82d868261..000000000
--- a/letsencrypt/LICENSE.txt
+++ /dev/null
@@ -1,205 +0,0 @@
-Let's Encrypt ACME Client
-Copyright (c) Electronic Frontier Foundation and others
-Licensed Apache Version 2.0
-
-The nginx plugin incorporates code from nginxparser
-Copyright (c) 2014 Fatih Erikli
-Licensed MIT
-
-
-Text of Apache 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.
-
-
-Text of MIT License
-===================
-Permission is hereby granted, free of charge, to any person obtaining a copy of
-this software and associated documentation files (the "Software"), to deal in
-the Software without restriction, including without limitation the rights to
-use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
-the Software, and to permit persons to whom the Software is furnished to do so,
-subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
-FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
-COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
-IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
-CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/letsencrypt/MANIFEST.in b/letsencrypt/MANIFEST.in
deleted file mode 100644
index 97e2ad3df..000000000
--- a/letsencrypt/MANIFEST.in
+++ /dev/null
@@ -1,2 +0,0 @@
-include LICENSE.txt
-include README.rst
diff --git a/letsencrypt/README.rst b/letsencrypt/README.rst
deleted file mode 100644
index b5fa0ec95..000000000
--- a/letsencrypt/README.rst
+++ /dev/null
@@ -1,2 +0,0 @@
-This package is a simple shim around the ``certbot`` ACME client for backwards
-compatibility.
diff --git a/letsencrypt/letsencrypt/__init__.py b/letsencrypt/letsencrypt/__init__.py
deleted file mode 100644
index a67d641f5..000000000
--- a/letsencrypt/letsencrypt/__init__.py
+++ /dev/null
@@ -1,8 +0,0 @@
-"""Let's Encrypt ACME client."""
-import sys
-
-
-import certbot
-
-
-sys.modules['letsencrypt'] = certbot
diff --git a/letsencrypt/setup.py b/letsencrypt/setup.py
deleted file mode 100644
index 4541e85fe..000000000
--- a/letsencrypt/setup.py
+++ /dev/null
@@ -1,62 +0,0 @@
-import codecs
-import os
-import sys
-
-from setuptools import setup
-from setuptools import find_packages
-
-
-def read_file(filename, encoding='utf8'):
- """Read unicode from given file."""
- with codecs.open(filename, encoding=encoding) as fd:
- return fd.read()
-
-
-here = os.path.abspath(os.path.dirname(__file__))
-readme = read_file(os.path.join(here, 'README.rst'))
-
-
-# This package is a simple shim around certbot
-install_requires = ['certbot']
-
-
-version = '0.8.0.dev0'
-
-
-setup(
- name='letsencrypt',
- version=version,
- description="ACME client",
- long_description=readme,
- url='https://github.com/letsencrypt/letsencrypt',
- author="Certbot Project",
- author_email='client-dev@letsencrypt.org',
- license='Apache License 2.0',
- classifiers=[
- 'Development Status :: 3 - Alpha',
- 'Environment :: Console',
- 'Environment :: Console :: Curses',
- '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',
- '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,
- entry_points={
- 'console_scripts': [
- 'letsencrypt = certbot.main:main',
- ],
- },
-)
diff --git a/letsencrypt/tests/testdata/os-release b/letsencrypt/tests/testdata/os-release
deleted file mode 100644
index b7c3ceb1b..000000000
--- a/letsencrypt/tests/testdata/os-release
+++ /dev/null
@@ -1,8 +0,0 @@
-NAME="SystemdOS"
-VERSION="42.42.42 LTS, Unreal"
-ID=systemdos
-ID_LIKE=debian
-PRETTY_NAME="SystemdOS 42.42.42 Unreal"
-VERSION_ID="42"
-HOME_URL="http://www.example.com/"
-SUPPORT_URL="http://help.example.com/"
diff --git a/letshelp-letsencrypt/LICENSE.txt b/letshelp-letsencrypt/LICENSE.txt
deleted file mode 100644
index 981c46c9f..000000000
--- a/letshelp-letsencrypt/LICENSE.txt
+++ /dev/null
@@ -1,190 +0,0 @@
- 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/letshelp-letsencrypt/MANIFEST.in b/letshelp-letsencrypt/MANIFEST.in
deleted file mode 100644
index 97e2ad3df..000000000
--- a/letshelp-letsencrypt/MANIFEST.in
+++ /dev/null
@@ -1,2 +0,0 @@
-include LICENSE.txt
-include README.rst
diff --git a/letshelp-letsencrypt/README.rst b/letshelp-letsencrypt/README.rst
deleted file mode 100644
index 57d0d8a3b..000000000
--- a/letshelp-letsencrypt/README.rst
+++ /dev/null
@@ -1,2 +0,0 @@
-This package is a simple shim around the ``letshelp-certbot`` for backwards
-compatibility.
diff --git a/letshelp-letsencrypt/letshelp_letsencrypt/__init__.py b/letshelp-letsencrypt/letshelp_letsencrypt/__init__.py
deleted file mode 100644
index fe4e272f9..000000000
--- a/letshelp-letsencrypt/letshelp_letsencrypt/__init__.py
+++ /dev/null
@@ -1,8 +0,0 @@
-"""Tools for submitting server configurations."""
-import sys
-
-
-import letshelp_certbot
-
-
-sys.modules['letshelp_letsencrypt'] = letshelp_certbot
diff --git a/letshelp-letsencrypt/setup.py b/letshelp-letsencrypt/setup.py
deleted file mode 100644
index 10380c49b..000000000
--- a/letshelp-letsencrypt/setup.py
+++ /dev/null
@@ -1,61 +0,0 @@
-import codecs
-import os
-import sys
-
-from setuptools import setup
-from setuptools import find_packages
-
-
-def read_file(filename, encoding='utf8'):
- """Read unicode from given file."""
- with codecs.open(filename, encoding=encoding) as fd:
- return fd.read()
-
-
-here = os.path.abspath(os.path.dirname(__file__))
-readme = read_file(os.path.join(here, 'README.rst'))
-
-
-version = '0.7.0.dev0'
-
-
-# This package is a simple shim around letshelp-certbot
-install_requires = ['letshelp-certbot']
-
-
-setup(
- name='letshelp-letsencrypt',
- version=version,
- description="Let's help Let's Encrypt client",
- long_description=readme,
- url='https://github.com/letsencrypt/letsencrypt',
- 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',
- '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,
- entry_points={
- 'console_scripts': [
- 'letshelp-letsencrypt-apache = letshelp_certbot.apache:main',
- ],
- },
-)
diff --git a/setup.py b/setup.py
index 6d0909ea8..4227d5d92 100644
--- a/setup.py
+++ b/setup.py
@@ -51,12 +51,6 @@ install_requires = [
'zope.interface',
]
-# Debian squeeze support, cf. #280
-if sys.version_info[0] == 2:
- install_requires.append('python2-pythondialog>=3.2.2rc1')
-else:
- install_requires.append('pythondialog>=3.2.2rc1')
-
# env markers in extras_require cause problems with older pip: #517
# Keep in sync with conditional_requirements.py.
if sys.version_info < (2, 7):
@@ -85,7 +79,6 @@ docs_extras = [
'repoze.sphinx.autointerface',
'Sphinx>=1.0', # autodoc_member_order = 'bysource', autodoc_default_flags
'sphinx_rtd_theme',
- 'sphinxcontrib-programoutput',
]
setup(
diff --git a/tests/boulder-integration.sh b/tests/boulder-integration.sh
index a70f13f8e..30fc17f81 100755
--- a/tests/boulder-integration.sh
+++ b/tests/boulder-integration.sh
@@ -33,18 +33,59 @@ common() {
"$@"
}
+export HOOK_TEST="/tmp/hook$$"
+CheckHooks() {
+ COMMON="wtf2.auth\nwtf2.cleanup\nrenew\nrenew"
+ EXPECTED="/tmp/expected$$"
+ if [ $(head -n1 $HOOK_TEST) = "wtf.pre" ]; then
+ echo "wtf.pre" > "$EXPECTED"
+ echo "wtf2.pre" >> "$EXPECTED"
+ echo $COMMON >> "$EXPECTED"
+ echo "wtf.post" >> "$EXPECTED"
+ echo "wtf2.post" >> "$EXPECTED"
+ else
+ echo "wtf2.pre" > "$EXPECTED"
+ echo "wtf.pre" >> "$EXPECTED"
+ echo $COMMON >> "$EXPECTED"
+ echo "wtf2.post" >> "$EXPECTED"
+ echo "wtf.post" >> "$EXPECTED"
+ fi
+
+ if cmp --quiet "$EXPECTED" "$HOOK_TEST" ; then
+ echo Hooks did not run as expected\; got
+ cat "$HOOK_TEST"
+ echo Expected
+ cat "$EXPECTED"
+ fi
+ rm "$HOOK_TEST"
+}
+
# We start a server listening on the port for the
# unrequested challenge to prevent regressions in #3601.
python -m SimpleHTTPServer $http_01_port &
python_server_pid=$!
-common --domains le1.wtf --preferred-challenges tls-sni-01 auth
+
+common --domains le1.wtf --preferred-challenges tls-sni-01 auth \
+ --pre-hook 'echo wtf.pre >> "$HOOK_TEST"' \
+ --post-hook 'echo wtf.post >> "$HOOK_TEST"'\
+ --renew-hook 'echo renew >> "$HOOK_TEST"'
kill $python_server_pid
python -m SimpleHTTPServer $tls_sni_01_port &
python_server_pid=$!
-common --domains le2.wtf --preferred-challenges http-01 run
+common --domains le2.wtf --preferred-challenges http-01 run \
+ --pre-hook 'echo wtf.pre >> "$HOOK_TEST"' \
+ --post-hook 'echo wtf.post >> "$HOOK_TEST"'\
+ --renew-hook 'echo renew >> "$HOOK_TEST"'
kill $python_server_pid
-common -a manual -d le.wtf auth --rsa-key-size 4096
+common certonly -a manual -d le.wtf --rsa-key-size 4096 \
+ --manual-auth-hook 'echo wtf2.auth >> "$HOOK_TEST" && ./tests/manual-http-auth.sh' \
+ --manual-cleanup-hook 'echo wtf2.cleanup >> "$HOOK_TEST" && ./tests/manual-http-cleanup.sh' \
+ --pre-hook 'echo wtf2.pre >> "$HOOK_TEST"' \
+ --post-hook 'echo wtf2.post >> "$HOOK_TEST"'
+
+common certonly -a manual -d dns.le.wtf --preferred-challenges dns-01 \
+ --manual-auth-hook ./tests/manual-dns-auth.sh
export CSR_PATH="${root}/csr.der" KEY_PATH="${root}/key.pem" \
OPENSSL_CNF=examples/openssl.cnf
@@ -73,8 +114,11 @@ common_no_force_renew renew
CheckCertCount 1
# --renew-by-default is used, so renewal should occur
+[ -f "$HOOK_TEST" ] && rm -f "$HOOK_TEST"
common renew
CheckCertCount 2
+CheckHooks
+
# This will renew because the expiry is less than 10 years from now
sed -i "4arenew_before_expiry = 4 years" "$root/conf/renewal/le.wtf.conf"
@@ -118,7 +162,14 @@ common revoke --cert-path "$root/conf/live/le1.wtf/cert.pem"
# revoke by cert key
common revoke --cert-path "$root/conf/live/le2.wtf/cert.pem" \
--key-path "$root/conf/live/le2.wtf/privkey.pem"
-
+# Get new certs to test revoke with a reason, by account and by cert key
+common --domains le1.wtf
+common revoke --cert-path "$root/conf/live/le1.wtf/cert.pem" \
+ --reason cessationOfOperation
+common --domains le2.wtf
+common revoke --cert-path "$root/conf/live/le2.wtf/cert.pem" \
+ --key-path "$root/conf/live/le2.wtf/privkey.pem" \
+ --reason keyCompromise
if type nginx;
then
. ./certbot-nginx/tests/boulder-integration.sh
diff --git a/tests/display.py b/tests/display.py
index ecb7c279b..1f548e33d 100644
--- a/tests/display.py
+++ b/tests/display.py
@@ -18,5 +18,5 @@ def test_visual(displayer, choices):
if __name__ == "__main__":
- for displayer in util.NcursesDisplay(), util.FileDisplay(sys.stdout):
- test_visual(displayer, util_test.CHOICES)
+ displayer = util.FileDisplay(sys.stdout, False)
+ test_visual(displayer, util_test.CHOICES)
diff --git a/tests/integration/_common.sh b/tests/integration/_common.sh
index 935d44994..9b44631d4 100755
--- a/tests/integration/_common.sh
+++ b/tests/integration/_common.sh
@@ -2,7 +2,7 @@
if [ "xxx$root" = "xxx" ];
then
- # The -t is required on OS X. It provides a template file path for
+ # The -t is required on macOS. It provides a template file path for
# the kernel to use.
root="$(mktemp -d -t leitXXXX)"
echo "Root integration tests directory: $root"
@@ -25,13 +25,13 @@ certbot_test_no_force_renew () {
--no-verify-ssl \
--tls-sni-01-port $tls_sni_01_port \
--http-01-port $http_01_port \
- --manual-test-mode \
+ --manual-public-ip-logging-ok \
$store_flags \
--non-interactive \
--no-redirect \
--agree-tos \
--register-unsafely-without-email \
--debug \
- -vvvvvvv \
+ -vv \
"$@"
}
diff --git a/tests/letstest/scripts/test_letsencrypt_auto_certonly_standalone.sh b/tests/letstest/scripts/test_letsencrypt_auto_certonly_standalone.sh
index f4aef11fe..9b5ff88a2 100755
--- a/tests/letstest/scripts/test_letsencrypt_auto_certonly_standalone.sh
+++ b/tests/letstest/scripts/test_letsencrypt_auto_certonly_standalone.sh
@@ -8,9 +8,31 @@
#private_ip=$(curl -s http://169.254.169.254/2014-11-05/meta-data/local-ipv4)
cd letsencrypt
-./letsencrypt-auto --os-packages-only --debug --version
-./letsencrypt-auto certonly --no-self-upgrade -v --standalone --debug \
+export PATH="$PWD/letsencrypt-auto-source:$PATH"
+letsencrypt-auto --os-packages-only --debug --version
+letsencrypt-auto certonly --no-self-upgrade -v --standalone --debug \
--text --agree-dev-preview --agree-tos \
--renew-by-default --redirect \
--register-unsafely-without-email \
--domain $PUBLIC_HOSTNAME --server $BOULDER_URL
+
+# we have to jump through some hoops to cope with relative paths in renewal
+# conf files ...
+# 1. be in the right directory
+cd tests/letstest/testdata/
+
+# 2. refer to the config with the same level of relativitity that it itself
+# contains :/
+OUT=`letsencrypt-auto certificates --config-dir sample-config -v`
+TEST_CERTS=`echo "$OUT" | grep TEST_CERT | wc -l`
+REVOKED=`echo "$OUT" | grep REVOKED | wc -l`
+
+if [ "$TEST_CERTS" != 2 ] ; then
+ echo "Did not find two test certs as expected ($TEST_CERTS)"
+ exit 1
+fi
+
+if [ "$REVOKED" != 1 ] ; then
+ echo "Did not find one revoked cert as expected ($REVOKED)"
+ exit 1
+fi
diff --git a/tests/letstest/scripts/test_letsencrypt_auto_venv_only.sh b/tests/letstest/scripts/test_letsencrypt_auto_venv_only.sh
index 234e70f68..c55e12e8b 100755
--- a/tests/letstest/scripts/test_letsencrypt_auto_venv_only.sh
+++ b/tests/letstest/scripts/test_letsencrypt_auto_venv_only.sh
@@ -4,4 +4,4 @@
cd letsencrypt
# help installs virtualenv and does nothing else
-./letsencrypt-auto -v --debug --help all
+./letsencrypt-auto-source/letsencrypt-auto -v --debug --help all
diff --git a/tests/letstest/scripts/test_ocsp_experimental.sh b/tests/letstest/scripts/test_ocsp_experimental.sh
new file mode 100755
index 000000000..cc787653c
--- /dev/null
+++ b/tests/letstest/scripts/test_ocsp_experimental.sh
@@ -0,0 +1,39 @@
+#!/bin/bash -x
+
+# $PUBLIC_IP $PRIVATE_IP $PUBLIC_HOSTNAME $BOULDER_URL are dynamically set at execution
+
+# with curl, instance metadata available from EC2 metadata service:
+#public_host=$(curl -s http://169.254.169.254/2014-11-05/meta-data/public-hostname)
+#public_ip=$(curl -s http://169.254.169.254/2014-11-05/meta-data/public-ipv4)
+#private_ip=$(curl -s http://169.254.169.254/2014-11-05/meta-data/local-ipv4)
+
+cd letsencrypt
+export PATH="$PWD/letsencrypt-auto-source:$PATH"
+letsencrypt-auto-source/letsencrypt-auto --os-packages-only --debug --version
+tools/venv.sh
+sudo venv/bin/certbot certonly --no-self-upgrade -v --standalone --debug \
+ --text --agree-dev-preview --agree-tos \
+ --renew-by-default --redirect \
+ --register-unsafely-without-email \
+ --domain $PUBLIC_HOSTNAME --server $BOULDER_URL
+
+# we have to jump through some hoops to cope with relative paths in renewal
+# conf files ...
+# 1. be in the right directory
+cd tests/letstest/testdata/
+
+# 2. refer to the config with the same level of relativitity that it itself
+# contains :/
+OUT=`sudo ../../../venv/bin/certbot certificates -v --config-dir sample-config`
+TEST_CERTS=`echo "$OUT" | grep TEST_CERT | wc -l`
+REVOKED=`echo "$OUT" | grep REVOKED | wc -l`
+
+if [ "$TEST_CERTS" != 2 ] ; then
+ echo "Did not find two test certs as expected ($TEST_CERTS)"
+ exit 1
+fi
+
+if [ "$REVOKED" != 1 ] ; then
+ echo "Did not find one revoked cert as expected ($REVOKED)"
+ exit 1
+fi
diff --git a/tests/letstest/testdata/sample-config/accounts/acme-staging.api.letsencrypt.org/directory/48d6b9e8d767eccf7e4d877d6ffa81e3/meta.json b/tests/letstest/testdata/sample-config/accounts/acme-staging.api.letsencrypt.org/directory/48d6b9e8d767eccf7e4d877d6ffa81e3/meta.json
new file mode 100644
index 000000000..6fe0b47f3
--- /dev/null
+++ b/tests/letstest/testdata/sample-config/accounts/acme-staging.api.letsencrypt.org/directory/48d6b9e8d767eccf7e4d877d6ffa81e3/meta.json
@@ -0,0 +1 @@
+{"creation_host": "ec2-52-91-193-99.compute-1.amazonaws.com", "creation_dt": "2016-12-23T02:08:32Z"}
\ No newline at end of file
diff --git a/tests/letstest/testdata/sample-config/accounts/acme-staging.api.letsencrypt.org/directory/48d6b9e8d767eccf7e4d877d6ffa81e3/private_key.json b/tests/letstest/testdata/sample-config/accounts/acme-staging.api.letsencrypt.org/directory/48d6b9e8d767eccf7e4d877d6ffa81e3/private_key.json
new file mode 100644
index 000000000..0affb573d
--- /dev/null
+++ b/tests/letstest/testdata/sample-config/accounts/acme-staging.api.letsencrypt.org/directory/48d6b9e8d767eccf7e4d877d6ffa81e3/private_key.json
@@ -0,0 +1 @@
+{"e": "AQAB", "d": "W410Wny96RO4qJ207KGQ3RSn0KAwqb93JBMHWU1yS9H3fN_2eCpFYdMLNFI9t1__nW1okeUioEfvMN_YW-G9krw97kVdZ63MfbeJCf35Onc8VZhAnk_3V8MtS26Of8ml0tTYhlQ65nuzhvHbY7aP-Uk260oDN-AbCCVhu5G4CQiMY6sdtCc8YkB6gK7SK874oWU7ogvAIPtNtEI-AXDUBYNAfoh34s1r2fE6mJSX4UYtzWB2hTUisvZdVL5JUInvxpCQFttk1cwWLFwwb6d2ERCbseeudvGJ6fkYiJ-EYxfHKOQK2kxPeOlLFMwGYQ0khDxTNajxQ1Asl43r7wgAeQ", "n": "xL5HzdhU_7P-_tphpRxpDSIL2L-aAlWt6r9EVyw53Sp-jx4fHDgnYv9HQOzNeL_IpLRCLLBItMzqnBvHUdHcS3aB6fv8HSNiHdVdC-c2rPFO8DLSGLNqi9G9WshjLDsKwc__BPNX5wHFcm8TZUJ4uZ_Ax1JCe05ePHWAf8GTr8vPaKtMpUVF55HPwpJtYvFZlH1LiVo8I_trJtHl8-pGeel3zdcaDJgNZrohZG2acTg95Ry46FE4HOslAg8Z6yECPyYLInJSDcb5yCgSqtOOp7rMVSPQFhoZRt4KDfew9lqIwNQSJoDE3bJWpwkzL1tp4clG8ExI1WnA86OjW83Vvw", "q": "0xdfHMMKYWHPE1UoQ10niDI7rnCM9vmPo4JpCOCYZf51KPNJgNaPCw62Q0Y-ZQfCBifypQyf291d0_2C_Rif0WMg07Y-Ypv8SpPK77vLV12GoAoAX2Xy3AJAz1gDBcyUzDtRlrzgCZja9YqIDVzMatkdPJXaBrBu5B-sXv4wGa0", "p": "7pl5xe_400Sn6PdN_F6KLWHFROVd7379WPWGHYmnvOvXx7DmrMjDsTOmhNRlrv7jPemVqMzp1FGsubGBizEMFGyCET30bUgH6ZU7Cmgv-2JKKN1FZnm1QTepZ7kjAT_qRCI6nvN6J0SIX197QOSz3hMmP7UYQXQ32QcVKdCksps", "kty": "RSA", "qi": "zG60VpLZjgR0o7dTeEP-HjbtxHUedyZLGe4FIPyWrPRl28anebkMUGzibpB8z5ohRsqHU2i4tmDq2NMvshISqkpk8t5PLiIcQgU46HQ24SCv7lunkVPKYU1n2uXVVfttrBP4c3UkjYzda1bcIVp6cJHanm_JuWI5nxy9ebVQJiw", "dp": "kRIBx0aj7Jh22x_aa9JzgypKDhzDY4W7tmX5-GWk9ioTVZgKeQ3MZiZ4XZTiimbxdchbNXn5xh0uvuzdTesxZA2he6hGwFcmcHBKqIY2fksBuhznQGpJuXCFcMpRLUZWQrzpFZIGOG_j1tEwGIG1lxXfkKakK8_k0PEMfhMcwHc", "dq": "AsoSRa0GHBdQxy6e45T9ir0vMLToB_NwRHbasHVXTjG4lpvwYrVzGnBNVEI_XNJna_FnMWsjSaJ5NO3qpzGGGxw2ONX1qRPql4mwas6Od08TElZPfvM37FRTSuoc0BzN8ozuHRHN3BKbAheciKCrStYnnr9ULDZ0oKsSegbd19k"}
\ No newline at end of file
diff --git a/tests/letstest/testdata/sample-config/accounts/acme-staging.api.letsencrypt.org/directory/48d6b9e8d767eccf7e4d877d6ffa81e3/regr.json b/tests/letstest/testdata/sample-config/accounts/acme-staging.api.letsencrypt.org/directory/48d6b9e8d767eccf7e4d877d6ffa81e3/regr.json
new file mode 100644
index 000000000..fdd2df7da
--- /dev/null
+++ b/tests/letstest/testdata/sample-config/accounts/acme-staging.api.letsencrypt.org/directory/48d6b9e8d767eccf7e4d877d6ffa81e3/regr.json
@@ -0,0 +1 @@
+{"body": {"agreement": "https://letsencrypt.org/documents/LE-SA-v1.1.1-August-1-2016.pdf", "key": {"e": "AQAB", "kty": "RSA", "n": "xL5HzdhU_7P-_tphpRxpDSIL2L-aAlWt6r9EVyw53Sp-jx4fHDgnYv9HQOzNeL_IpLRCLLBItMzqnBvHUdHcS3aB6fv8HSNiHdVdC-c2rPFO8DLSGLNqi9G9WshjLDsKwc__BPNX5wHFcm8TZUJ4uZ_Ax1JCe05ePHWAf8GTr8vPaKtMpUVF55HPwpJtYvFZlH1LiVo8I_trJtHl8-pGeel3zdcaDJgNZrohZG2acTg95Ry46FE4HOslAg8Z6yECPyYLInJSDcb5yCgSqtOOp7rMVSPQFhoZRt4KDfew9lqIwNQSJoDE3bJWpwkzL1tp4clG8ExI1WnA86OjW83Vvw"}}, "uri": "https://acme-staging.api.letsencrypt.org/acme/reg/566631", "new_authzr_uri": "https://acme-staging.api.letsencrypt.org/acme/new-authz", "terms_of_service": "https://letsencrypt.org/documents/LE-SA-v1.1.1-August-1-2016.pdf"}
\ No newline at end of file
diff --git a/tests/letstest/testdata/sample-config/archive/a.encryption-example.com/cert1.pem b/tests/letstest/testdata/sample-config/archive/a.encryption-example.com/cert1.pem
new file mode 100644
index 000000000..80739dd3f
--- /dev/null
+++ b/tests/letstest/testdata/sample-config/archive/a.encryption-example.com/cert1.pem
@@ -0,0 +1,29 @@
+-----BEGIN CERTIFICATE-----
+MIIE9TCCA92gAwIBAgITAPrA8hxQOlpVRMgAm/Ib0HYdqzANBgkqhkiG9w0BAQsF
+ADAiMSAwHgYDVQQDDBdGYWtlIExFIEludGVybWVkaWF0ZSBYMTAeFw0xNjEyMjMw
+MTAyMDBaFw0xNzAzMjMwMTAyMDBaMCMxITAfBgNVBAMTGGEuZW5jcnlwdGlvbi1l
+eGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKqz0cco
+hsCqyWPwGr79a8j+JO3HqbphLTzhoNHYF+fW8glyMyBmOMyZjc8v8E3U3KYEXuuR
+WzR+bvUXBcLOhSogIifZDNiMKEFyDNcDlG08ze9GTj2hTQyjet2ZuPWNuuJ4u5UM
+FvobaceDqITuqEqUrjCBi5CmEXswrV3l2BVSiOcPf+l+ZR81xG7qcjGfLG6YQWca
+nsYYorz/kSRtwYjAT4NaeUYNXVeH1luWTWhbed8pmKfBVfv+OEmwUyAhSE1ePfny
+Cj37wo1+nqQz37IJNEpI0RNbxrE7ZCgA40QrFVqc9XevcypFi9DftVWzDNBtd97Q
+lmHuIqA9Kb3C/e8CAwEAAaOCAiEwggIdMA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUE
+FjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQU
+C7/XcCnNRht91hnQVEB2E9AtNUowHwYDVR0jBBgwFoAUwMwDRrlYIMxccnDz4S7L
+IKb1aDoweAYIKwYBBQUHAQEEbDBqMDMGCCsGAQUFBzABhidodHRwOi8vb2NzcC5z
+dGctaW50LXgxLmxldHNlbmNyeXB0Lm9yZy8wMwYIKwYBBQUHMAKGJ2h0dHA6Ly9j
+ZXJ0LnN0Zy1pbnQteDEubGV0c2VuY3J5cHQub3JnLzAjBgNVHREEHDAaghhhLmVu
+Y3J5cHRpb24tZXhhbXBsZS5jb20wgf4GA1UdIASB9jCB8zAIBgZngQwBAgEwgeYG
+CysGAQQBgt8TAQEBMIHWMCYGCCsGAQUFBwIBFhpodHRwOi8vY3BzLmxldHNlbmNy
+eXB0Lm9yZzCBqwYIKwYBBQUHAgIwgZ4MgZtUaGlzIENlcnRpZmljYXRlIG1heSBv
+bmx5IGJlIHJlbGllZCB1cG9uIGJ5IFJlbHlpbmcgUGFydGllcyBhbmQgb25seSBp
+biBhY2NvcmRhbmNlIHdpdGggdGhlIENlcnRpZmljYXRlIFBvbGljeSBmb3VuZCBh
+dCBodHRwczovL2xldHNlbmNyeXB0Lm9yZy9yZXBvc2l0b3J5LzANBgkqhkiG9w0B
+AQsFAAOCAQEAP04z87VVNYYHpBkCLkw3B+gTd/F0xDo7ab2HvJJAeOpZgSfoSYMR
+omYWiug9wGQqKjs4kaOGjAkW1EV3qosumOtvK7uTvoa2caXDjPYAxRiVIp08Qm0J
+/FU/FfGpUXBZW9Ne3m3nDYxOCAWAw9WmV+dUuvb7qZWQSKs7cQv3FY/NuQe0o9LH
+FgL7T0W7vc6uVGeBgcoEkX7xX4T7A9V3BqL6mgkK+L++n0EFrDXXzWWENNdWYCvY
+Ptu0Ez95IyYNRgI3U1waO9QZ944Pc9OuMCZD4ifbYoMKGqSQb3sGR+B2TQ+qqCUC
+4sikdX4WRbEYKlBTcvSpCVJ7ndFTyD6lyg==
+-----END CERTIFICATE-----
diff --git a/tests/letstest/testdata/sample-config/archive/a.encryption-example.com/chain1.pem b/tests/letstest/testdata/sample-config/archive/a.encryption-example.com/chain1.pem
new file mode 100644
index 000000000..29a54e2a1
--- /dev/null
+++ b/tests/letstest/testdata/sample-config/archive/a.encryption-example.com/chain1.pem
@@ -0,0 +1,27 @@
+-----BEGIN CERTIFICATE-----
+MIIEqzCCApOgAwIBAgIRAIvhKg5ZRO08VGQx8JdhT+UwDQYJKoZIhvcNAQELBQAw
+GjEYMBYGA1UEAwwPRmFrZSBMRSBSb290IFgxMB4XDTE2MDUyMzIyMDc1OVoXDTM2
+MDUyMzIyMDc1OVowIjEgMB4GA1UEAwwXRmFrZSBMRSBJbnRlcm1lZGlhdGUgWDEw
+ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDtWKySDn7rWZc5ggjz3ZB0
+8jO4xti3uzINfD5sQ7Lj7hzetUT+wQob+iXSZkhnvx+IvdbXF5/yt8aWPpUKnPym
+oLxsYiI5gQBLxNDzIec0OIaflWqAr29m7J8+NNtApEN8nZFnf3bhehZW7AxmS1m0
+ZnSsdHw0Fw+bgixPg2MQ9k9oefFeqa+7Kqdlz5bbrUYV2volxhDFtnI4Mh8BiWCN
+xDH1Hizq+GKCcHsinDZWurCqder/afJBnQs+SBSL6MVApHt+d35zjBD92fO2Je56
+dhMfzCgOKXeJ340WhW3TjD1zqLZXeaCyUNRnfOmWZV8nEhtHOFbUCU7r/KkjMZO9
+AgMBAAGjgeMwgeAwDgYDVR0PAQH/BAQDAgGGMBIGA1UdEwEB/wQIMAYBAf8CAQAw
+HQYDVR0OBBYEFMDMA0a5WCDMXHJw8+EuyyCm9Wg6MHoGCCsGAQUFBwEBBG4wbDA0
+BggrBgEFBQcwAYYoaHR0cDovL29jc3Auc3RnLXJvb3QteDEubGV0c2VuY3J5cHQu
+b3JnLzA0BggrBgEFBQcwAoYoaHR0cDovL2NlcnQuc3RnLXJvb3QteDEubGV0c2Vu
+Y3J5cHQub3JnLzAfBgNVHSMEGDAWgBTBJnSkikSg5vogKNhcI5pFiBh54DANBgkq
+hkiG9w0BAQsFAAOCAgEABYSu4Il+fI0MYU42OTmEj+1HqQ5DvyAeyCA6sGuZdwjF
+UGeVOv3NnLyfofuUOjEbY5irFCDtnv+0ckukUZN9lz4Q2YjWGUpW4TTu3ieTsaC9
+AFvCSgNHJyWSVtWvB5XDxsqawl1KzHzzwr132bF2rtGtazSqVqK9E07sGHMCf+zp
+DQVDVVGtqZPHwX3KqUtefE621b8RI6VCl4oD30Olf8pjuzG4JKBFRFclzLRjo/h7
+IkkfjZ8wDa7faOjVXx6n+eUQ29cIMCzr8/rNWHS9pYGGQKJiY2xmVC9h12H99Xyf
+zWE9vb5zKP3MVG6neX1hSdo7PEAb9fqRhHkqVsqUvJlIRmvXvVKTwNCP3eCjRCCI
+PTAvjV+4ni786iXwwFYNz8l3PmPLCyQXWGohnJ8iBm+5nk7O2ynaPVW0U2W+pt2w
+SVuvdDM5zGv2f9ltNWUiYZHJ1mmO97jSY/6YfdOUH66iRtQtDkHBRdkNBsMbD+Em
+2TgBldtHNSJBfB3pm9FblgOcJ0FSWcUDWJ7vO0+NTXlgrRofRT6pVywzxVo6dND0
+WzYlTWeUVsO40xJqhgUQRER9YLOLxJ0O6C8i0xFxAMKOtSdodMB3RIwt7RFQ0uyt
+n5Z5MqkYhlMI3J1tPRTp1nEt9fyGspBOO05gi148Qasp+3N+svqKomoQglNoAxU=
+-----END CERTIFICATE-----
diff --git a/tests/letstest/testdata/sample-config/archive/a.encryption-example.com/fullchain1.pem b/tests/letstest/testdata/sample-config/archive/a.encryption-example.com/fullchain1.pem
new file mode 100644
index 000000000..ba245d213
--- /dev/null
+++ b/tests/letstest/testdata/sample-config/archive/a.encryption-example.com/fullchain1.pem
@@ -0,0 +1,56 @@
+-----BEGIN CERTIFICATE-----
+MIIE9TCCA92gAwIBAgITAPrA8hxQOlpVRMgAm/Ib0HYdqzANBgkqhkiG9w0BAQsF
+ADAiMSAwHgYDVQQDDBdGYWtlIExFIEludGVybWVkaWF0ZSBYMTAeFw0xNjEyMjMw
+MTAyMDBaFw0xNzAzMjMwMTAyMDBaMCMxITAfBgNVBAMTGGEuZW5jcnlwdGlvbi1l
+eGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKqz0cco
+hsCqyWPwGr79a8j+JO3HqbphLTzhoNHYF+fW8glyMyBmOMyZjc8v8E3U3KYEXuuR
+WzR+bvUXBcLOhSogIifZDNiMKEFyDNcDlG08ze9GTj2hTQyjet2ZuPWNuuJ4u5UM
+FvobaceDqITuqEqUrjCBi5CmEXswrV3l2BVSiOcPf+l+ZR81xG7qcjGfLG6YQWca
+nsYYorz/kSRtwYjAT4NaeUYNXVeH1luWTWhbed8pmKfBVfv+OEmwUyAhSE1ePfny
+Cj37wo1+nqQz37IJNEpI0RNbxrE7ZCgA40QrFVqc9XevcypFi9DftVWzDNBtd97Q
+lmHuIqA9Kb3C/e8CAwEAAaOCAiEwggIdMA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUE
+FjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQU
+C7/XcCnNRht91hnQVEB2E9AtNUowHwYDVR0jBBgwFoAUwMwDRrlYIMxccnDz4S7L
+IKb1aDoweAYIKwYBBQUHAQEEbDBqMDMGCCsGAQUFBzABhidodHRwOi8vb2NzcC5z
+dGctaW50LXgxLmxldHNlbmNyeXB0Lm9yZy8wMwYIKwYBBQUHMAKGJ2h0dHA6Ly9j
+ZXJ0LnN0Zy1pbnQteDEubGV0c2VuY3J5cHQub3JnLzAjBgNVHREEHDAaghhhLmVu
+Y3J5cHRpb24tZXhhbXBsZS5jb20wgf4GA1UdIASB9jCB8zAIBgZngQwBAgEwgeYG
+CysGAQQBgt8TAQEBMIHWMCYGCCsGAQUFBwIBFhpodHRwOi8vY3BzLmxldHNlbmNy
+eXB0Lm9yZzCBqwYIKwYBBQUHAgIwgZ4MgZtUaGlzIENlcnRpZmljYXRlIG1heSBv
+bmx5IGJlIHJlbGllZCB1cG9uIGJ5IFJlbHlpbmcgUGFydGllcyBhbmQgb25seSBp
+biBhY2NvcmRhbmNlIHdpdGggdGhlIENlcnRpZmljYXRlIFBvbGljeSBmb3VuZCBh
+dCBodHRwczovL2xldHNlbmNyeXB0Lm9yZy9yZXBvc2l0b3J5LzANBgkqhkiG9w0B
+AQsFAAOCAQEAP04z87VVNYYHpBkCLkw3B+gTd/F0xDo7ab2HvJJAeOpZgSfoSYMR
+omYWiug9wGQqKjs4kaOGjAkW1EV3qosumOtvK7uTvoa2caXDjPYAxRiVIp08Qm0J
+/FU/FfGpUXBZW9Ne3m3nDYxOCAWAw9WmV+dUuvb7qZWQSKs7cQv3FY/NuQe0o9LH
+FgL7T0W7vc6uVGeBgcoEkX7xX4T7A9V3BqL6mgkK+L++n0EFrDXXzWWENNdWYCvY
+Ptu0Ez95IyYNRgI3U1waO9QZ944Pc9OuMCZD4ifbYoMKGqSQb3sGR+B2TQ+qqCUC
+4sikdX4WRbEYKlBTcvSpCVJ7ndFTyD6lyg==
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIEqzCCApOgAwIBAgIRAIvhKg5ZRO08VGQx8JdhT+UwDQYJKoZIhvcNAQELBQAw
+GjEYMBYGA1UEAwwPRmFrZSBMRSBSb290IFgxMB4XDTE2MDUyMzIyMDc1OVoXDTM2
+MDUyMzIyMDc1OVowIjEgMB4GA1UEAwwXRmFrZSBMRSBJbnRlcm1lZGlhdGUgWDEw
+ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDtWKySDn7rWZc5ggjz3ZB0
+8jO4xti3uzINfD5sQ7Lj7hzetUT+wQob+iXSZkhnvx+IvdbXF5/yt8aWPpUKnPym
+oLxsYiI5gQBLxNDzIec0OIaflWqAr29m7J8+NNtApEN8nZFnf3bhehZW7AxmS1m0
+ZnSsdHw0Fw+bgixPg2MQ9k9oefFeqa+7Kqdlz5bbrUYV2volxhDFtnI4Mh8BiWCN
+xDH1Hizq+GKCcHsinDZWurCqder/afJBnQs+SBSL6MVApHt+d35zjBD92fO2Je56
+dhMfzCgOKXeJ340WhW3TjD1zqLZXeaCyUNRnfOmWZV8nEhtHOFbUCU7r/KkjMZO9
+AgMBAAGjgeMwgeAwDgYDVR0PAQH/BAQDAgGGMBIGA1UdEwEB/wQIMAYBAf8CAQAw
+HQYDVR0OBBYEFMDMA0a5WCDMXHJw8+EuyyCm9Wg6MHoGCCsGAQUFBwEBBG4wbDA0
+BggrBgEFBQcwAYYoaHR0cDovL29jc3Auc3RnLXJvb3QteDEubGV0c2VuY3J5cHQu
+b3JnLzA0BggrBgEFBQcwAoYoaHR0cDovL2NlcnQuc3RnLXJvb3QteDEubGV0c2Vu
+Y3J5cHQub3JnLzAfBgNVHSMEGDAWgBTBJnSkikSg5vogKNhcI5pFiBh54DANBgkq
+hkiG9w0BAQsFAAOCAgEABYSu4Il+fI0MYU42OTmEj+1HqQ5DvyAeyCA6sGuZdwjF
+UGeVOv3NnLyfofuUOjEbY5irFCDtnv+0ckukUZN9lz4Q2YjWGUpW4TTu3ieTsaC9
+AFvCSgNHJyWSVtWvB5XDxsqawl1KzHzzwr132bF2rtGtazSqVqK9E07sGHMCf+zp
+DQVDVVGtqZPHwX3KqUtefE621b8RI6VCl4oD30Olf8pjuzG4JKBFRFclzLRjo/h7
+IkkfjZ8wDa7faOjVXx6n+eUQ29cIMCzr8/rNWHS9pYGGQKJiY2xmVC9h12H99Xyf
+zWE9vb5zKP3MVG6neX1hSdo7PEAb9fqRhHkqVsqUvJlIRmvXvVKTwNCP3eCjRCCI
+PTAvjV+4ni786iXwwFYNz8l3PmPLCyQXWGohnJ8iBm+5nk7O2ynaPVW0U2W+pt2w
+SVuvdDM5zGv2f9ltNWUiYZHJ1mmO97jSY/6YfdOUH66iRtQtDkHBRdkNBsMbD+Em
+2TgBldtHNSJBfB3pm9FblgOcJ0FSWcUDWJ7vO0+NTXlgrRofRT6pVywzxVo6dND0
+WzYlTWeUVsO40xJqhgUQRER9YLOLxJ0O6C8i0xFxAMKOtSdodMB3RIwt7RFQ0uyt
+n5Z5MqkYhlMI3J1tPRTp1nEt9fyGspBOO05gi148Qasp+3N+svqKomoQglNoAxU=
+-----END CERTIFICATE-----
diff --git a/tests/letstest/testdata/sample-config/archive/a.encryption-example.com/privkey1.pem b/tests/letstest/testdata/sample-config/archive/a.encryption-example.com/privkey1.pem
new file mode 100644
index 000000000..b3059cb47
--- /dev/null
+++ b/tests/letstest/testdata/sample-config/archive/a.encryption-example.com/privkey1.pem
@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCqs9HHKIbAqslj
+8Bq+/WvI/iTtx6m6YS084aDR2Bfn1vIJcjMgZjjMmY3PL/BN1NymBF7rkVs0fm71
+FwXCzoUqICIn2QzYjChBcgzXA5RtPM3vRk49oU0Mo3rdmbj1jbrieLuVDBb6G2nH
+g6iE7qhKlK4wgYuQphF7MK1d5dgVUojnD3/pfmUfNcRu6nIxnyxumEFnGp7GGKK8
+/5EkbcGIwE+DWnlGDV1Xh9Zblk1oW3nfKZinwVX7/jhJsFMgIUhNXj358go9+8KN
+fp6kM9+yCTRKSNETW8axO2QoAONEKxVanPV3r3MqRYvQ37VVswzQbXfe0JZh7iKg
+PSm9wv3vAgMBAAECggEAattP6Wz8FaWTlgTaqU44Z8R314VSQULNr7vKETJFnLKY
+JsOfL5vt2F4TQGxQ8Ffcm+xGgw4l2tF+odv8ljrzbzBYUTt06CWsmXNMiFhMVKlo
+fG01Uy0i71Ny+T9eYhCLuXM8cYv04jHA4M0Q8831+WHjPKgLdswOS2BoVkwoHQfc
+xEo40D0sPynd+KRukhgR+5AjwMdaNOV7S8c5iuQYIaZ1Xe5AyfiQkMV4LdbobMDj
+bHzGxdeC5GRVOHnMBYrRotgSt4+bsQGeoV9yWY0WAVvnoDfRBRdWK8yRVhuJY1+D
+WB6sPJ5cOg7Ijclubo9b+EaUkddvP0aCA3FepqNwcQKBgQDR0hz9OSom2fBjLaR2
+mQe3LqnotwPCuMmXuKndGIwJz9KgelBaRNUcvDtnzSzQVZ3h9/YFJKUkoVPVCoAu
+wAF9aBeDGs+LdHerBK8fI87PXwCV0OlZLQfUw1/82dpO/dyYXVeGorrO6FE/Oxb8
+enLerMW0Ocp/MhEgM5lFRUJM1wKBgQDQRauI9QuMoBnl516pOs+7EPRvTwe4oBpO
+iH2U7ryJ/YQTgsx25sDWqQBouEnv3j83wnVh9kApkS8UXFd4ZwuizIFCMlgrxw4x
+nKDsd1TZOLUO2FNi09YWPUnzxzQBOjBeekEIDKUQCLOKttTrjRHgGld3tmVtHWtL
+W+OvNIdcqQKBgCMpqjAJr3W5Wl7UnFY/yRo62MCmQxwT6bzidp0V6woN6Qd52BN4
+q5pYNUBtExCK+J2Q94rfHEnqO2ldjCPJi7ZfhmkzSgrd5twjOdHnJ1Z7Xla9Hw4R
+zNksMN7oB3zrcFecdPmcNeBM8Ki/F1gSkUOeArf0Y2ozkskpvIruU3EbAoGBAMVz
+h7CMQKrNjj/8Hi5qZ05+QH7Wegd7IfWaSRTNUUmxY2nr81Q2aFQaXRzquo4CMgT3
+Arog76t4zR2MfhDUAKATKehMOnMmgDpgt9/3MiXOMTkltchX9PuYl2faT19qfzjS
+xpyPAF43IaA8vZejYnMIBiyka3wLDBGhyDXuovYhAoGAB/AZnOM/4SQuIdtzmBSy
+YsHpXcNgRPqvfauCus3e5I6H4wmi+nqF/jyt0oyDBDKZki67CpStwu5Eo7tcLLnY
+o+VfJ9co8jUfVxRh0NlZwomF1t/8yAm/deWoV9sX9Yj71ft/eomCifNseeeg31Kl
+wkqKc3PndJHrR40mswUOHbs=
+-----END PRIVATE KEY-----
diff --git a/tests/letstest/testdata/sample-config/archive/b.encryption-example.com/cert1.pem b/tests/letstest/testdata/sample-config/archive/b.encryption-example.com/cert1.pem
new file mode 100644
index 000000000..0c1c6b5ef
--- /dev/null
+++ b/tests/letstest/testdata/sample-config/archive/b.encryption-example.com/cert1.pem
@@ -0,0 +1,29 @@
+-----BEGIN CERTIFICATE-----
+MIIE9TCCA92gAwIBAgITAPqBl0IgXf6F9LO/8sV1SsoA9DANBgkqhkiG9w0BAQsF
+ADAiMSAwHgYDVQQDDBdGYWtlIExFIEludGVybWVkaWF0ZSBYMTAeFw0xNjEyMjMw
+MTA0MDBaFw0xNzAzMjMwMTA0MDBaMCMxITAfBgNVBAMTGGIuZW5jcnlwdGlvbi1l
+eGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALWA6tWR
+FAfYyOEM9HtJXK4tCd1tGF2QZrlJHEL3PJzFHonv7ZaPo6Vkrar1uLinM4AVux/f
+s9vcsbdebu54DXpj1IllzjKs3tjStHK46luMqj8gf+3yLZIIVnN4YxkItd1WBtim
++144ku1gULsGnnHmuCefXz6qqkLzFZsElqO7NY+TL4F4m/L0lDjYsU++XgbHT9gi
+Tw0jAi8SyH8Ia4IYi4ynnMuHuS11e+yOtq16kLW1RdnxrYpleu9z0DU+6Xlr1tbl
+eSkyzbWelDgdsicfOxZz5pbmALXErb472TidcHHK6bsMVhR/P1zQK9Ydc+tC33d0
+XCRRgPoduN8XRfcCAwEAAaOCAiEwggIdMA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUE
+FjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQU
+RJ6J6HcpXRdRjqfyGshMEzkJy4cwHwYDVR0jBBgwFoAUwMwDRrlYIMxccnDz4S7L
+IKb1aDoweAYIKwYBBQUHAQEEbDBqMDMGCCsGAQUFBzABhidodHRwOi8vb2NzcC5z
+dGctaW50LXgxLmxldHNlbmNyeXB0Lm9yZy8wMwYIKwYBBQUHMAKGJ2h0dHA6Ly9j
+ZXJ0LnN0Zy1pbnQteDEubGV0c2VuY3J5cHQub3JnLzAjBgNVHREEHDAaghhiLmVu
+Y3J5cHRpb24tZXhhbXBsZS5jb20wgf4GA1UdIASB9jCB8zAIBgZngQwBAgEwgeYG
+CysGAQQBgt8TAQEBMIHWMCYGCCsGAQUFBwIBFhpodHRwOi8vY3BzLmxldHNlbmNy
+eXB0Lm9yZzCBqwYIKwYBBQUHAgIwgZ4MgZtUaGlzIENlcnRpZmljYXRlIG1heSBv
+bmx5IGJlIHJlbGllZCB1cG9uIGJ5IFJlbHlpbmcgUGFydGllcyBhbmQgb25seSBp
+biBhY2NvcmRhbmNlIHdpdGggdGhlIENlcnRpZmljYXRlIFBvbGljeSBmb3VuZCBh
+dCBodHRwczovL2xldHNlbmNyeXB0Lm9yZy9yZXBvc2l0b3J5LzANBgkqhkiG9w0B
+AQsFAAOCAQEA2K8R+nSf9TmfSeUqB+ckObkf8bgyR0qKx/8fGoYGNAzKVE0KUs8u
+SDIITjbcTivEuSChycZAGQMEMZal8uT8GsFqqJUcEJUzuxbv7nvZkCSdal1PrRsw
+U4cBBuuZ/NvisEZCyjZe8mMdlhcSgThzqljF5Tcz3EWvaH9kxhqr8eL/6pYdAasT
+0HqirveIQUrf9LqEEAYGB3P6VI2kjroxUZif7dt2jvOGwJEJfHOjiC8rp0Db0hVZ
+omXSsZN6mVkbv1q0I7lgKWu1RHfNAefado3TJZHe8JJ5Oxrl3f2hxi3SzuPGgfXV
+ZdKb0zjDXhgumrp0F2eT9zltTIUr8alYcg==
+-----END CERTIFICATE-----
diff --git a/tests/letstest/testdata/sample-config/archive/b.encryption-example.com/chain1.pem b/tests/letstest/testdata/sample-config/archive/b.encryption-example.com/chain1.pem
new file mode 100644
index 000000000..29a54e2a1
--- /dev/null
+++ b/tests/letstest/testdata/sample-config/archive/b.encryption-example.com/chain1.pem
@@ -0,0 +1,27 @@
+-----BEGIN CERTIFICATE-----
+MIIEqzCCApOgAwIBAgIRAIvhKg5ZRO08VGQx8JdhT+UwDQYJKoZIhvcNAQELBQAw
+GjEYMBYGA1UEAwwPRmFrZSBMRSBSb290IFgxMB4XDTE2MDUyMzIyMDc1OVoXDTM2
+MDUyMzIyMDc1OVowIjEgMB4GA1UEAwwXRmFrZSBMRSBJbnRlcm1lZGlhdGUgWDEw
+ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDtWKySDn7rWZc5ggjz3ZB0
+8jO4xti3uzINfD5sQ7Lj7hzetUT+wQob+iXSZkhnvx+IvdbXF5/yt8aWPpUKnPym
+oLxsYiI5gQBLxNDzIec0OIaflWqAr29m7J8+NNtApEN8nZFnf3bhehZW7AxmS1m0
+ZnSsdHw0Fw+bgixPg2MQ9k9oefFeqa+7Kqdlz5bbrUYV2volxhDFtnI4Mh8BiWCN
+xDH1Hizq+GKCcHsinDZWurCqder/afJBnQs+SBSL6MVApHt+d35zjBD92fO2Je56
+dhMfzCgOKXeJ340WhW3TjD1zqLZXeaCyUNRnfOmWZV8nEhtHOFbUCU7r/KkjMZO9
+AgMBAAGjgeMwgeAwDgYDVR0PAQH/BAQDAgGGMBIGA1UdEwEB/wQIMAYBAf8CAQAw
+HQYDVR0OBBYEFMDMA0a5WCDMXHJw8+EuyyCm9Wg6MHoGCCsGAQUFBwEBBG4wbDA0
+BggrBgEFBQcwAYYoaHR0cDovL29jc3Auc3RnLXJvb3QteDEubGV0c2VuY3J5cHQu
+b3JnLzA0BggrBgEFBQcwAoYoaHR0cDovL2NlcnQuc3RnLXJvb3QteDEubGV0c2Vu
+Y3J5cHQub3JnLzAfBgNVHSMEGDAWgBTBJnSkikSg5vogKNhcI5pFiBh54DANBgkq
+hkiG9w0BAQsFAAOCAgEABYSu4Il+fI0MYU42OTmEj+1HqQ5DvyAeyCA6sGuZdwjF
+UGeVOv3NnLyfofuUOjEbY5irFCDtnv+0ckukUZN9lz4Q2YjWGUpW4TTu3ieTsaC9
+AFvCSgNHJyWSVtWvB5XDxsqawl1KzHzzwr132bF2rtGtazSqVqK9E07sGHMCf+zp
+DQVDVVGtqZPHwX3KqUtefE621b8RI6VCl4oD30Olf8pjuzG4JKBFRFclzLRjo/h7
+IkkfjZ8wDa7faOjVXx6n+eUQ29cIMCzr8/rNWHS9pYGGQKJiY2xmVC9h12H99Xyf
+zWE9vb5zKP3MVG6neX1hSdo7PEAb9fqRhHkqVsqUvJlIRmvXvVKTwNCP3eCjRCCI
+PTAvjV+4ni786iXwwFYNz8l3PmPLCyQXWGohnJ8iBm+5nk7O2ynaPVW0U2W+pt2w
+SVuvdDM5zGv2f9ltNWUiYZHJ1mmO97jSY/6YfdOUH66iRtQtDkHBRdkNBsMbD+Em
+2TgBldtHNSJBfB3pm9FblgOcJ0FSWcUDWJ7vO0+NTXlgrRofRT6pVywzxVo6dND0
+WzYlTWeUVsO40xJqhgUQRER9YLOLxJ0O6C8i0xFxAMKOtSdodMB3RIwt7RFQ0uyt
+n5Z5MqkYhlMI3J1tPRTp1nEt9fyGspBOO05gi148Qasp+3N+svqKomoQglNoAxU=
+-----END CERTIFICATE-----
diff --git a/tests/letstest/testdata/sample-config/archive/b.encryption-example.com/fullchain1.pem b/tests/letstest/testdata/sample-config/archive/b.encryption-example.com/fullchain1.pem
new file mode 100644
index 000000000..705cca6c3
--- /dev/null
+++ b/tests/letstest/testdata/sample-config/archive/b.encryption-example.com/fullchain1.pem
@@ -0,0 +1,56 @@
+-----BEGIN CERTIFICATE-----
+MIIE9TCCA92gAwIBAgITAPqBl0IgXf6F9LO/8sV1SsoA9DANBgkqhkiG9w0BAQsF
+ADAiMSAwHgYDVQQDDBdGYWtlIExFIEludGVybWVkaWF0ZSBYMTAeFw0xNjEyMjMw
+MTA0MDBaFw0xNzAzMjMwMTA0MDBaMCMxITAfBgNVBAMTGGIuZW5jcnlwdGlvbi1l
+eGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALWA6tWR
+FAfYyOEM9HtJXK4tCd1tGF2QZrlJHEL3PJzFHonv7ZaPo6Vkrar1uLinM4AVux/f
+s9vcsbdebu54DXpj1IllzjKs3tjStHK46luMqj8gf+3yLZIIVnN4YxkItd1WBtim
++144ku1gULsGnnHmuCefXz6qqkLzFZsElqO7NY+TL4F4m/L0lDjYsU++XgbHT9gi
+Tw0jAi8SyH8Ia4IYi4ynnMuHuS11e+yOtq16kLW1RdnxrYpleu9z0DU+6Xlr1tbl
+eSkyzbWelDgdsicfOxZz5pbmALXErb472TidcHHK6bsMVhR/P1zQK9Ydc+tC33d0
+XCRRgPoduN8XRfcCAwEAAaOCAiEwggIdMA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUE
+FjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQU
+RJ6J6HcpXRdRjqfyGshMEzkJy4cwHwYDVR0jBBgwFoAUwMwDRrlYIMxccnDz4S7L
+IKb1aDoweAYIKwYBBQUHAQEEbDBqMDMGCCsGAQUFBzABhidodHRwOi8vb2NzcC5z
+dGctaW50LXgxLmxldHNlbmNyeXB0Lm9yZy8wMwYIKwYBBQUHMAKGJ2h0dHA6Ly9j
+ZXJ0LnN0Zy1pbnQteDEubGV0c2VuY3J5cHQub3JnLzAjBgNVHREEHDAaghhiLmVu
+Y3J5cHRpb24tZXhhbXBsZS5jb20wgf4GA1UdIASB9jCB8zAIBgZngQwBAgEwgeYG
+CysGAQQBgt8TAQEBMIHWMCYGCCsGAQUFBwIBFhpodHRwOi8vY3BzLmxldHNlbmNy
+eXB0Lm9yZzCBqwYIKwYBBQUHAgIwgZ4MgZtUaGlzIENlcnRpZmljYXRlIG1heSBv
+bmx5IGJlIHJlbGllZCB1cG9uIGJ5IFJlbHlpbmcgUGFydGllcyBhbmQgb25seSBp
+biBhY2NvcmRhbmNlIHdpdGggdGhlIENlcnRpZmljYXRlIFBvbGljeSBmb3VuZCBh
+dCBodHRwczovL2xldHNlbmNyeXB0Lm9yZy9yZXBvc2l0b3J5LzANBgkqhkiG9w0B
+AQsFAAOCAQEA2K8R+nSf9TmfSeUqB+ckObkf8bgyR0qKx/8fGoYGNAzKVE0KUs8u
+SDIITjbcTivEuSChycZAGQMEMZal8uT8GsFqqJUcEJUzuxbv7nvZkCSdal1PrRsw
+U4cBBuuZ/NvisEZCyjZe8mMdlhcSgThzqljF5Tcz3EWvaH9kxhqr8eL/6pYdAasT
+0HqirveIQUrf9LqEEAYGB3P6VI2kjroxUZif7dt2jvOGwJEJfHOjiC8rp0Db0hVZ
+omXSsZN6mVkbv1q0I7lgKWu1RHfNAefado3TJZHe8JJ5Oxrl3f2hxi3SzuPGgfXV
+ZdKb0zjDXhgumrp0F2eT9zltTIUr8alYcg==
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIEqzCCApOgAwIBAgIRAIvhKg5ZRO08VGQx8JdhT+UwDQYJKoZIhvcNAQELBQAw
+GjEYMBYGA1UEAwwPRmFrZSBMRSBSb290IFgxMB4XDTE2MDUyMzIyMDc1OVoXDTM2
+MDUyMzIyMDc1OVowIjEgMB4GA1UEAwwXRmFrZSBMRSBJbnRlcm1lZGlhdGUgWDEw
+ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDtWKySDn7rWZc5ggjz3ZB0
+8jO4xti3uzINfD5sQ7Lj7hzetUT+wQob+iXSZkhnvx+IvdbXF5/yt8aWPpUKnPym
+oLxsYiI5gQBLxNDzIec0OIaflWqAr29m7J8+NNtApEN8nZFnf3bhehZW7AxmS1m0
+ZnSsdHw0Fw+bgixPg2MQ9k9oefFeqa+7Kqdlz5bbrUYV2volxhDFtnI4Mh8BiWCN
+xDH1Hizq+GKCcHsinDZWurCqder/afJBnQs+SBSL6MVApHt+d35zjBD92fO2Je56
+dhMfzCgOKXeJ340WhW3TjD1zqLZXeaCyUNRnfOmWZV8nEhtHOFbUCU7r/KkjMZO9
+AgMBAAGjgeMwgeAwDgYDVR0PAQH/BAQDAgGGMBIGA1UdEwEB/wQIMAYBAf8CAQAw
+HQYDVR0OBBYEFMDMA0a5WCDMXHJw8+EuyyCm9Wg6MHoGCCsGAQUFBwEBBG4wbDA0
+BggrBgEFBQcwAYYoaHR0cDovL29jc3Auc3RnLXJvb3QteDEubGV0c2VuY3J5cHQu
+b3JnLzA0BggrBgEFBQcwAoYoaHR0cDovL2NlcnQuc3RnLXJvb3QteDEubGV0c2Vu
+Y3J5cHQub3JnLzAfBgNVHSMEGDAWgBTBJnSkikSg5vogKNhcI5pFiBh54DANBgkq
+hkiG9w0BAQsFAAOCAgEABYSu4Il+fI0MYU42OTmEj+1HqQ5DvyAeyCA6sGuZdwjF
+UGeVOv3NnLyfofuUOjEbY5irFCDtnv+0ckukUZN9lz4Q2YjWGUpW4TTu3ieTsaC9
+AFvCSgNHJyWSVtWvB5XDxsqawl1KzHzzwr132bF2rtGtazSqVqK9E07sGHMCf+zp
+DQVDVVGtqZPHwX3KqUtefE621b8RI6VCl4oD30Olf8pjuzG4JKBFRFclzLRjo/h7
+IkkfjZ8wDa7faOjVXx6n+eUQ29cIMCzr8/rNWHS9pYGGQKJiY2xmVC9h12H99Xyf
+zWE9vb5zKP3MVG6neX1hSdo7PEAb9fqRhHkqVsqUvJlIRmvXvVKTwNCP3eCjRCCI
+PTAvjV+4ni786iXwwFYNz8l3PmPLCyQXWGohnJ8iBm+5nk7O2ynaPVW0U2W+pt2w
+SVuvdDM5zGv2f9ltNWUiYZHJ1mmO97jSY/6YfdOUH66iRtQtDkHBRdkNBsMbD+Em
+2TgBldtHNSJBfB3pm9FblgOcJ0FSWcUDWJ7vO0+NTXlgrRofRT6pVywzxVo6dND0
+WzYlTWeUVsO40xJqhgUQRER9YLOLxJ0O6C8i0xFxAMKOtSdodMB3RIwt7RFQ0uyt
+n5Z5MqkYhlMI3J1tPRTp1nEt9fyGspBOO05gi148Qasp+3N+svqKomoQglNoAxU=
+-----END CERTIFICATE-----
diff --git a/tests/letstest/testdata/sample-config/archive/b.encryption-example.com/privkey1.pem b/tests/letstest/testdata/sample-config/archive/b.encryption-example.com/privkey1.pem
new file mode 100644
index 000000000..c43af4f50
--- /dev/null
+++ b/tests/letstest/testdata/sample-config/archive/b.encryption-example.com/privkey1.pem
@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQC1gOrVkRQH2Mjh
+DPR7SVyuLQndbRhdkGa5SRxC9zycxR6J7+2Wj6OlZK2q9bi4pzOAFbsf37Pb3LG3
+Xm7ueA16Y9SJZc4yrN7Y0rRyuOpbjKo/IH/t8i2SCFZzeGMZCLXdVgbYpvteOJLt
+YFC7Bp5x5rgnn18+qqpC8xWbBJajuzWPky+BeJvy9JQ42LFPvl4Gx0/YIk8NIwIv
+Esh/CGuCGIuMp5zLh7ktdXvsjratepC1tUXZ8a2KZXrvc9A1Pul5a9bW5XkpMs21
+npQ4HbInHzsWc+aW5gC1xK2+O9k4nXBxyum7DFYUfz9c0CvWHXPrQt93dFwkUYD6
+HbjfF0X3AgMBAAECggEAYjEnWnjNTF10d4Qps5UBxdzpzFfb6apYWH78AiJ9MRbX
+Kaqab2ywDKdF6Qpcb9FM5EtdW6YLSLPBlUFKZEqgiAkAD4D7J6EsQkLjinkNmI+l
+/tbXPuRY0PsfwgJsIjv7H44N0CGuNdAHdNI5eqTfDSHTmOP4hA+SYvvdQWsfD94r
+m4ocr2YfL4BmEh3hujb8NjVD8csSnFlpeVibtJ1rWiv1otLaEuVmcN49n0rIj0IK
+tiCIdqqIscVZ+P3fFfr/E3oL2nhBqxRnzqoK/HNTpI4JJAbRGP51nVr0QhZYpIuj
+xDM+zeuIt0lMYOzoE+JD0612Q66mokBPHZAd5MuEwQKBgQDbdJUQfcw/9zHuWm4n
+9+wYgMN1QhfJNEr21LUjbe551YapkU389mBJJIlmjH5p67PaMRuJ1o6uRJWv40hf
+Y4xy6iViLc1FExIvRVznxMCIyCELtuvYMiCJtaekFKunziniw8yg5SwSZJY3GlXN
+cDAwIcgb9PPU5rBEip8g0DIp1wKBgQDTunF3OtEoVqdsPSmw5y1767YTCsm3dnVT
++kwp7ZrX3TJ3Xd6EVPWUBP1HbGD3qfsIR+Ha3Vl8OiLNC4zDoZY886U4qY5Mtn4P
+JhUN0H9zYZg2l9gFf9u8RkUoPZPXXuk+eQnlGT133PrkCloDlqP47u/fQ5dV1t6F
+NghgwfOA4QKBgHI/IRMyylBKmj3h6hL4qHqhHiA/Ri7DAHu7hIlrQ4k9ths0wAr/
+IGUzlixC29S8libzBckeX60tm1ez1QuDwaxZZRjVi1V4djERxSoLbchHl5yHoAQv
+JG1Mmnd7I1n6pCefkzn31JfGscUB+sU2sH9+NrUHMqEVb5JfMDRe7p6FAoGAcYGc
+Xqz7gEKkUtSfSyVELxD4dVDtPxuUXsbqmfe1cVA2Q+Pg7NSXKxlZpzak7WEFITVY
+EXtlA8Iu8fnlJuOzpU2BH9VWYi3beseRtew2x2Zksa/JsXkQFekeHiqU3XsWU9WT
+xmw3ldCz+BjMlOvnUAbYNbsIoI4mkQecijKwFkECgYA2zafSyWCW5zAronUBQDEe
+vJumAJ77TwpYzzvH2ic6siWimdePxQ6TgdM3s1FgpdkbaXgKzS5MbZbD0Uyg3MEj
+t6ZT7GSWq39wLDJVDYJ5ClAi8mv9WNs8X8rJ0CkdiPZgHC77OwBELthGn2p9ncar
+Bwhs4S84KEJFT0LAC3YeRQ==
+-----END PRIVATE KEY-----
diff --git a/tests/letstest/testdata/sample-config/csr/0000_csr-certbot.pem b/tests/letstest/testdata/sample-config/csr/0000_csr-certbot.pem
new file mode 100644
index 000000000..16d73ffde
--- /dev/null
+++ b/tests/letstest/testdata/sample-config/csr/0000_csr-certbot.pem
@@ -0,0 +1,16 @@
+-----BEGIN CERTIFICATE REQUEST-----
+MIIChjCCAW4CAQIwFzEVMBMGA1UEAwwMaXMuaXNub3Qub3JnMIIBIjANBgkqhkiG
+9w0BAQEFAAOCAQ8AMIIBCgKCAQEA7nsHOCTvvQlRYXpI5xE7AggqTVmM8lGi18Y2
+gVlr3WYAS7higHRJjWroAmZ2Bx9IRfHOxwhVWm/hlc/u4w0IYlRnArg6suXrgtn+
+6Ea0WDUCiKEiKvQqD0kaI936hpydU/dY70UZnpKSyi0kiCrLzCkIaXS8HJdLOIXB
+Q4FMVqjppYjUejMgrabthq1QTqU0S4MxwS1oj67VqaAkedGWxFgFQ2kIFV0/WL13
+Xs0SCTYyN96KK1Q2CF63HoN79zc+TVslg32DDU5UF7sVVvlkoHcl0OgR9l4jfou5
+HwmatMjXPI+0bWVxmw6iC6tbK7Dx+ytYIodhEOL52Youzy/lLwIDAQABoCowKAYJ
+KoZIhvcNAQkOMRswGTAXBgNVHREEEDAOggxpcy5pc25vdC5vcmcwDQYJKoZIhvcN
+AQELBQADggEBAAJsLiylvGq64wxVt8EBeXRB4ycBzC5J/pyOWMP9oexW1o3XPhCC
++0tIQVGk7wJMe3+WiPMVsn4pGOUGDaPvfC7ijlvipzaYyLEfnr+J7pukhYbzNHmu
+XL5lbTJ0hTCfqUjmi1yE4M/v2eX5yNaEHsZExZ1NbtwutE/Tx5iSqt7kxbIoFqmF
+7Tne2JHjt945+/l9yvqaIcEFOmblS0OxY9EjxgJdhKCKbhD/ZoYaVVisc52h/2/M
+jtzvzZr1rZCvFnuQxGDco5vYe3u7uJ9tQHLCMpoIorT3kX3yTdgnWxst6XBVUY/P
+Q6O18obG4ALoP/ESzvTauQIwFVGfal/jqyI=
+-----END CERTIFICATE REQUEST-----
diff --git a/tests/letstest/testdata/sample-config/csr/0001_csr-certbot.pem b/tests/letstest/testdata/sample-config/csr/0001_csr-certbot.pem
new file mode 100644
index 000000000..452bc45cd
--- /dev/null
+++ b/tests/letstest/testdata/sample-config/csr/0001_csr-certbot.pem
@@ -0,0 +1,16 @@
+-----BEGIN CERTIFICATE REQUEST-----
+MIICgDCCAWgCAQIwFDESMBAGA1UEAwwJaXNub3Qub3JnMIIBIjANBgkqhkiG9w0B
+AQEFAAOCAQ8AMIIBCgKCAQEAsEAy7rdPsYFFt9VsK9NZy+W9nbsYGmvIaMSyJkEg
+Xe2P0MmnWG/hn6F1bLPm85uS5oQsOWDpwVz31tKhoWhUDbRzPWP5Ur2NnHY92Whz
+5tP4ir4vEEDuB9etQ8+wZ7+3z9q1VhPcgDdYyouQVB0QejJ1yUBiVPr289bW//ln
+kj9DFxn4oufoJ4ELSZSZgWFM92EGKMMy1zD2bJH87mI0Gs0pIOEo+QMJ8TvVEbau
++aFaTANslqRAF5LaWcrPgvHor7cK5w/4bVBZCmY2QYKqlYwZiRPpwg3Ii6B9Q8kz
+rDkGSDjwsazca4api57cza13XkRl7KvyZbwTwlFBud+ydwIDAQABoCcwJQYJKoZI
+hvcNAQkOMRgwFjAUBgNVHREEDTALgglpc25vdC5vcmcwDQYJKoZIhvcNAQELBQAD
+ggEBAB3vniZw2ML6E9jrMY8DtQjPDDNr1BqOGzyOaJipqpGZSRvhTA44DAAjdFpS
+5BLrnXniPIZGG4/6WorLTEDBnlFcLinUg7GDT2DpauQa+4PLxFi13hE1TuSVOp9A
+08YXhzALvZxMIjQ/tVhAp0+PkGEWU2wI0SmDvUUTJqMwSJYgXkf/vBS34/koKywV
+gPDod5AbLuhYgKiQYwDZ0dd69leT0REmizuaHtA6tW3mBgewSKotwqY3fHmhHV8o
+YLSVhImz4jJjK3LjmcdXuBxqE0z+p6n/+lSGG8RR/E8pix4OAkVAP6nyt/loW1BX
+ZzWOuSHozGN5UJSL248vLFWrsV8=
+-----END CERTIFICATE REQUEST-----
diff --git a/tests/letstest/testdata/sample-config/csr/0002_csr-certbot.pem b/tests/letstest/testdata/sample-config/csr/0002_csr-certbot.pem
new file mode 100644
index 000000000..2ee44b3fd
--- /dev/null
+++ b/tests/letstest/testdata/sample-config/csr/0002_csr-certbot.pem
@@ -0,0 +1,17 @@
+-----BEGIN CERTIFICATE REQUEST-----
+MIICnjCCAYYCAQIwIzEhMB8GA1UEAwwYYS5lbmNyeXB0aW9uLWV4YW1wbGUuY29t
+MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqrPRxyiGwKrJY/Aavv1r
+yP4k7cepumEtPOGg0dgX59byCXIzIGY4zJmNzy/wTdTcpgRe65FbNH5u9RcFws6F
+KiAiJ9kM2IwoQXIM1wOUbTzN70ZOPaFNDKN63Zm49Y264ni7lQwW+htpx4OohO6o
+SpSuMIGLkKYRezCtXeXYFVKI5w9/6X5lHzXEbupyMZ8sbphBZxqexhiivP+RJG3B
+iMBPg1p5Rg1dV4fWW5ZNaFt53ymYp8FV+/44SbBTICFITV49+fIKPfvCjX6epDPf
+sgk0SkjRE1vGsTtkKADjRCsVWpz1d69zKkWL0N+1VbMM0G133tCWYe4ioD0pvcL9
+7wIDAQABoDYwNAYJKoZIhvcNAQkOMScwJTAjBgNVHREEHDAaghhhLmVuY3J5cHRp
+b24tZXhhbXBsZS5jb20wDQYJKoZIhvcNAQELBQADggEBAJyKJHdUwR9BOKYJarUy
+P8mqu6UBUt8faSu6o3EUeDHbnUgxGAVwB5TJV0+JwIjPFQFRofHE8CFhUvi0W0YJ
+BsGVqblnJzz80NkUX9uwjBAGKaDxXqXDOctkQSAOJxM/rvD2uJLmlokibDDm7mnS
+DX8SUVAPgORDGlVTGATjvmA3YeH05gHRFgRDWFP5DOZs99fx4957HrXhsIxew98s
+Felupgswnouyq3crrgcjY0qo3Pc5gjUcuwaT2cjtvzi93f/ImDt6f1sdSSJB00wk
+34lbs/Z+0G8bH1dqYIZzkwNgq7rolhDYh3WRgTlfkgkV7FlkQGm8qn5uoQvaXaaS
+ShM=
+-----END CERTIFICATE REQUEST-----
diff --git a/tests/letstest/testdata/sample-config/csr/0003_csr-certbot.pem b/tests/letstest/testdata/sample-config/csr/0003_csr-certbot.pem
new file mode 100644
index 000000000..2a50dc33d
--- /dev/null
+++ b/tests/letstest/testdata/sample-config/csr/0003_csr-certbot.pem
@@ -0,0 +1,17 @@
+-----BEGIN CERTIFICATE REQUEST-----
+MIICnjCCAYYCAQIwIzEhMB8GA1UEAwwYYi5lbmNyeXB0aW9uLWV4YW1wbGUuY29t
+MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtYDq1ZEUB9jI4Qz0e0lc
+ri0J3W0YXZBmuUkcQvc8nMUeie/tlo+jpWStqvW4uKczgBW7H9+z29yxt15u7ngN
+emPUiWXOMqze2NK0crjqW4yqPyB/7fItkghWc3hjGQi13VYG2Kb7XjiS7WBQuwae
+cea4J59fPqqqQvMVmwSWo7s1j5MvgXib8vSUONixT75eBsdP2CJPDSMCLxLIfwhr
+ghiLjKecy4e5LXV77I62rXqQtbVF2fGtimV673PQNT7peWvW1uV5KTLNtZ6UOB2y
+Jx87FnPmluYAtcStvjvZOJ1wccrpuwxWFH8/XNAr1h1z60Lfd3RcJFGA+h243xdF
+9wIDAQABoDYwNAYJKoZIhvcNAQkOMScwJTAjBgNVHREEHDAaghhiLmVuY3J5cHRp
+b24tZXhhbXBsZS5jb20wDQYJKoZIhvcNAQELBQADggEBACDw8/zjFaIdp4aqyrzT
+fzaqAnoXZt3+0JDPLANy3DLCJmK2TQMyItg/Oid5NEQ45UluXv811IMCcONyVmrD
+19W3XErhTJOJMgpjg4GLBRRFhLm+uTIcbv/xEeUgOYbslsqwi2gHECe1Vsj/Ahbo
+QXXqcDg1cXe6VTQhX+Nw5q30t/oCmkJWcUVHBON2nbOujRz1+z6AjVl1dM+CYDRq
+bsKn7m3biYS7lx7/ApIuhJQsghcmccCtWrH5GsOUsJUgiANv5u+QZgGaajkCRKYV
+fD/u8qTPfKb/+lTxtDrfFOGH+mbZKbKf2/ibneYcql8fFQWiapbudI2cMk8yDxA9
+2Tw=
+-----END CERTIFICATE REQUEST-----
diff --git a/tests/letstest/testdata/sample-config/keys/0000_key-certbot.pem b/tests/letstest/testdata/sample-config/keys/0000_key-certbot.pem
new file mode 100644
index 000000000..9a018c41e
--- /dev/null
+++ b/tests/letstest/testdata/sample-config/keys/0000_key-certbot.pem
@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDuewc4JO+9CVFh
+ekjnETsCCCpNWYzyUaLXxjaBWWvdZgBLuGKAdEmNaugCZnYHH0hF8c7HCFVab+GV
+z+7jDQhiVGcCuDqy5euC2f7oRrRYNQKIoSIq9CoPSRoj3fqGnJ1T91jvRRmekpLK
+LSSIKsvMKQhpdLwcl0s4hcFDgUxWqOmliNR6MyCtpu2GrVBOpTRLgzHBLWiPrtWp
+oCR50ZbEWAVDaQgVXT9YvXdezRIJNjI33oorVDYIXrceg3v3Nz5NWyWDfYMNTlQX
+uxVW+WSgdyXQ6BH2XiN+i7kfCZq0yNc8j7RtZXGbDqILq1srsPH7K1gih2EQ4vnZ
+ii7PL+UvAgMBAAECggEBAIX9jeLXrfNSRu0z3b4mCjdsCwiGphCIGayOa5VlfptY
+chYZNQ7jR2gzhsPCedIqm1rhL8LYRcyYS/D2cUwUyH8m2PHIPQLC9/3/KZ+sCiv9
+LL1De4USxobsFcnNMLNtT2Ab+1YERw63X85EauAu226MJ3PI6OBPiS3qyNl6zj9p
+do9SyzsNFEGtDk+ndWf3keoHBKLge4DP1lA3Jt42wSUxVv9U5SLvFpMQm8PqbqrK
+4ofXcgxMFIJHDDGXsoDI7LOOsV6ncBVlui0ELM/QWBb5x1605VxqEDRL+h/wMp5Y
+JIc6HbgcERmtHmyFlHHNtjAXxeulJVDJQDekd/irJ5ECgYEA/WQJ4LwkkA/Yhf2W
+WYJtD8LuwzRnvGs3R+rgx3+hOeO4TFZD5fzObZVRSwWQO2jbOtBJOaRLUsUngcJQ
+DXr/FGf1rnGhLmNeLE+jN9FS73wBhEXViFZ/fzhVibGbc7u45Y5REykZj8HtUHP5
+hBKR2Nx94WDiv1MBgcKrRk6yI50CgYEA8O+vWcMzEdPtonHl8UgTa8/c5g/RBBvS
+plB8mVsmM/E5CNwnetZM32cg7dC7yNaZzn3qF6w+LdE2vw3j5VbqvuVUvsRgvYcJ
+3kMbHsbsxkRw+HVWZGgEtWNzuYQUL0xN+xzIZDWkbtuaihqYAy4voYNAM08BTNcE
+POQEMIGxcDsCgYEAg+TLo3grS/WDjhM2bHcQT9D2uRMRIClqx/uBbzaG9HwNFWcd
+xpv102KSwwstTU9CNfXu95sGPhozez5qrumj1rpaTqgE7wF4JnZ5jfdeRRv2KiSz
+hlkH2m+3TontUauYDZ0rpF6TWJnn7iW/7jhARHJY77SfslkBgsqSnnEeFp0CgYEA
+7FsFVvZRzCRt01UOsPL28mWYmyxa7D/rFvKQONUdFgmG3PUz2aIPCX2e5Q1GmlBD
+1Djbg1uaJ9I8dZJHxbzNTnWk+/ujt2mYuax1F20n65xKgsKA/MC6FcM5TH2QW5Hs
+UfI7d2rUI1hVMzPBeiU93qDmQy825E1uP9mjbn5cNe8CgYAsBpJgS1LkDruyWmjG
+ZTzdHGciA1O3gUArLQmyUfJlPS3Hgwn7wnBBihtGZDHmjJ7734+PQ9ioCnO9Pb+K
+8Cp29vJ85lka7o7I48OeScLmczgEUYOPCrbkkKJdKaG6gn5CKpRBVYDlhbWjVZ51
+4uda/BQ1hqHh8WmxK6x21qC9JQ==
+-----END PRIVATE KEY-----
diff --git a/tests/letstest/testdata/sample-config/keys/0001_key-certbot.pem b/tests/letstest/testdata/sample-config/keys/0001_key-certbot.pem
new file mode 100644
index 000000000..a3a7faf55
--- /dev/null
+++ b/tests/letstest/testdata/sample-config/keys/0001_key-certbot.pem
@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCwQDLut0+xgUW3
+1Wwr01nL5b2duxgaa8hoxLImQSBd7Y/QyadYb+GfoXVss+bzm5LmhCw5YOnBXPfW
+0qGhaFQNtHM9Y/lSvY2cdj3ZaHPm0/iKvi8QQO4H161Dz7Bnv7fP2rVWE9yAN1jK
+i5BUHRB6MnXJQGJU+vbz1tb/+WeSP0MXGfii5+gngQtJlJmBYUz3YQYowzLXMPZs
+kfzuYjQazSkg4Sj5AwnxO9URtq75oVpMA2yWpEAXktpZys+C8eivtwrnD/htUFkK
+ZjZBgqqVjBmJE+nCDciLoH1DyTOsOQZIOPCxrNxrhqmLntzNrXdeRGXsq/JlvBPC
+UUG537J3AgMBAAECggEBAJoZR27X72GvORmmDFG1FInlcIf8EPLo0exoLaqsvnPh
+RSCzbxEvoQFE1boZARB1MVdCsLfqN/bMJhU5TAAni3YAE9HVGyRwfuQRrbnsTYnA
+Q0prRhLb8kIBHIhxijbrtPaSroF4FA42VfehVqt0TffJLpqrJE5QrqI7cPeVRCzk
+laLyi2rjZBhN6l1OxFSIOrEDlcowlPUMORbmNDMbq/dLu5riVO/kP2x70K1IiANI
+NZzVhMwkktYj3Ku2altRLcyRrC3Bs46w2QF6wiC88/LMapt79um65P/SgcCgyOYE
+oxJywZwMnyw8ut1Y+KS8B7AdzqWmj7Q9wr0xbW6+4eECgYEA6sNrMGZVRUFRPAcr
+m3y5fkM/WJ8tAkT3hI2/noljv3k8iameTy/B/y3p+aM8/6Oa/gdO/SWtfKPednkf
+CIh/3J5tJ1yvK7wHEEU6r6qxVKr2FLCMfSXoGx+E+r9qPF8WdV+55beVgO86UqA5
+y9a6DhNA+Xt4jDJc+rbpga0pj60CgYEAwDHDV0lR7jVT6iiU6VhAu1gM/SBVqXE/
+VSfmGihgaO4pJ9OgfqusKbraNONc+oBub7B4T3sSnF/I0mSUclD6brmG99OWLIg8
+L6/ed+bLPRO0iTvKRLbyBLom1Totfh/X6iQ2Zci40vLIS7kbYDban16ca+iSm+0B
+41RV4q6+vzMCgYBLoxiW6HGStZ+xonHHT+EHsCzppac/su64c18IeiV8HFiH1fFe
+e/mZ+LYIqzJM/u5B6CLn5srFfJqBOzbnbescLqLmarM5eQQhltx4mps1tzs/oT4y
+WBM3IembTC6zMsOun1/qhkKR3wHAe0UDyrP5MvTdLI3DRbq1QFdtY1gfpQKBgEgg
+pNGWJ5RBGSvwbOohf7GPOtioEN3VLVJ09crtSjk23+Uda8b+AE9s20Ur6pHsLwXl
+cVFKu9JJtCEZNAiu0T1KjRdmpZ4yxnuTAed3iuByC7fQ43jkO3GAtuAgxD/oDWzG
+iE+sg4hPKtIYNujlzSgwJn3su1CfIq1A0jaPI/C3AoGAHGTBtsXdR1goFvcxwA+n
+l2bAs/InoED5nj26a//JuONgtGlm//QKCxIgjjktpeZm8sfsaYeR+rwIUODWRX/e
+LUF85a70SaH+FZRXBRS2d/zaNxO4F37nE5fwO+VAurSb7El7yOyCepK22iSHMYdl
+xak78KZKv3HXW5yrfA+dc2Y=
+-----END PRIVATE KEY-----
diff --git a/tests/letstest/testdata/sample-config/keys/0002_key-certbot.pem b/tests/letstest/testdata/sample-config/keys/0002_key-certbot.pem
new file mode 100644
index 000000000..b3059cb47
--- /dev/null
+++ b/tests/letstest/testdata/sample-config/keys/0002_key-certbot.pem
@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCqs9HHKIbAqslj
+8Bq+/WvI/iTtx6m6YS084aDR2Bfn1vIJcjMgZjjMmY3PL/BN1NymBF7rkVs0fm71
+FwXCzoUqICIn2QzYjChBcgzXA5RtPM3vRk49oU0Mo3rdmbj1jbrieLuVDBb6G2nH
+g6iE7qhKlK4wgYuQphF7MK1d5dgVUojnD3/pfmUfNcRu6nIxnyxumEFnGp7GGKK8
+/5EkbcGIwE+DWnlGDV1Xh9Zblk1oW3nfKZinwVX7/jhJsFMgIUhNXj358go9+8KN
+fp6kM9+yCTRKSNETW8axO2QoAONEKxVanPV3r3MqRYvQ37VVswzQbXfe0JZh7iKg
+PSm9wv3vAgMBAAECggEAattP6Wz8FaWTlgTaqU44Z8R314VSQULNr7vKETJFnLKY
+JsOfL5vt2F4TQGxQ8Ffcm+xGgw4l2tF+odv8ljrzbzBYUTt06CWsmXNMiFhMVKlo
+fG01Uy0i71Ny+T9eYhCLuXM8cYv04jHA4M0Q8831+WHjPKgLdswOS2BoVkwoHQfc
+xEo40D0sPynd+KRukhgR+5AjwMdaNOV7S8c5iuQYIaZ1Xe5AyfiQkMV4LdbobMDj
+bHzGxdeC5GRVOHnMBYrRotgSt4+bsQGeoV9yWY0WAVvnoDfRBRdWK8yRVhuJY1+D
+WB6sPJ5cOg7Ijclubo9b+EaUkddvP0aCA3FepqNwcQKBgQDR0hz9OSom2fBjLaR2
+mQe3LqnotwPCuMmXuKndGIwJz9KgelBaRNUcvDtnzSzQVZ3h9/YFJKUkoVPVCoAu
+wAF9aBeDGs+LdHerBK8fI87PXwCV0OlZLQfUw1/82dpO/dyYXVeGorrO6FE/Oxb8
+enLerMW0Ocp/MhEgM5lFRUJM1wKBgQDQRauI9QuMoBnl516pOs+7EPRvTwe4oBpO
+iH2U7ryJ/YQTgsx25sDWqQBouEnv3j83wnVh9kApkS8UXFd4ZwuizIFCMlgrxw4x
+nKDsd1TZOLUO2FNi09YWPUnzxzQBOjBeekEIDKUQCLOKttTrjRHgGld3tmVtHWtL
+W+OvNIdcqQKBgCMpqjAJr3W5Wl7UnFY/yRo62MCmQxwT6bzidp0V6woN6Qd52BN4
+q5pYNUBtExCK+J2Q94rfHEnqO2ldjCPJi7ZfhmkzSgrd5twjOdHnJ1Z7Xla9Hw4R
+zNksMN7oB3zrcFecdPmcNeBM8Ki/F1gSkUOeArf0Y2ozkskpvIruU3EbAoGBAMVz
+h7CMQKrNjj/8Hi5qZ05+QH7Wegd7IfWaSRTNUUmxY2nr81Q2aFQaXRzquo4CMgT3
+Arog76t4zR2MfhDUAKATKehMOnMmgDpgt9/3MiXOMTkltchX9PuYl2faT19qfzjS
+xpyPAF43IaA8vZejYnMIBiyka3wLDBGhyDXuovYhAoGAB/AZnOM/4SQuIdtzmBSy
+YsHpXcNgRPqvfauCus3e5I6H4wmi+nqF/jyt0oyDBDKZki67CpStwu5Eo7tcLLnY
+o+VfJ9co8jUfVxRh0NlZwomF1t/8yAm/deWoV9sX9Yj71ft/eomCifNseeeg31Kl
+wkqKc3PndJHrR40mswUOHbs=
+-----END PRIVATE KEY-----
diff --git a/tests/letstest/testdata/sample-config/keys/0003_key-certbot.pem b/tests/letstest/testdata/sample-config/keys/0003_key-certbot.pem
new file mode 100644
index 000000000..c43af4f50
--- /dev/null
+++ b/tests/letstest/testdata/sample-config/keys/0003_key-certbot.pem
@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQC1gOrVkRQH2Mjh
+DPR7SVyuLQndbRhdkGa5SRxC9zycxR6J7+2Wj6OlZK2q9bi4pzOAFbsf37Pb3LG3
+Xm7ueA16Y9SJZc4yrN7Y0rRyuOpbjKo/IH/t8i2SCFZzeGMZCLXdVgbYpvteOJLt
+YFC7Bp5x5rgnn18+qqpC8xWbBJajuzWPky+BeJvy9JQ42LFPvl4Gx0/YIk8NIwIv
+Esh/CGuCGIuMp5zLh7ktdXvsjratepC1tUXZ8a2KZXrvc9A1Pul5a9bW5XkpMs21
+npQ4HbInHzsWc+aW5gC1xK2+O9k4nXBxyum7DFYUfz9c0CvWHXPrQt93dFwkUYD6
+HbjfF0X3AgMBAAECggEAYjEnWnjNTF10d4Qps5UBxdzpzFfb6apYWH78AiJ9MRbX
+Kaqab2ywDKdF6Qpcb9FM5EtdW6YLSLPBlUFKZEqgiAkAD4D7J6EsQkLjinkNmI+l
+/tbXPuRY0PsfwgJsIjv7H44N0CGuNdAHdNI5eqTfDSHTmOP4hA+SYvvdQWsfD94r
+m4ocr2YfL4BmEh3hujb8NjVD8csSnFlpeVibtJ1rWiv1otLaEuVmcN49n0rIj0IK
+tiCIdqqIscVZ+P3fFfr/E3oL2nhBqxRnzqoK/HNTpI4JJAbRGP51nVr0QhZYpIuj
+xDM+zeuIt0lMYOzoE+JD0612Q66mokBPHZAd5MuEwQKBgQDbdJUQfcw/9zHuWm4n
+9+wYgMN1QhfJNEr21LUjbe551YapkU389mBJJIlmjH5p67PaMRuJ1o6uRJWv40hf
+Y4xy6iViLc1FExIvRVznxMCIyCELtuvYMiCJtaekFKunziniw8yg5SwSZJY3GlXN
+cDAwIcgb9PPU5rBEip8g0DIp1wKBgQDTunF3OtEoVqdsPSmw5y1767YTCsm3dnVT
++kwp7ZrX3TJ3Xd6EVPWUBP1HbGD3qfsIR+Ha3Vl8OiLNC4zDoZY886U4qY5Mtn4P
+JhUN0H9zYZg2l9gFf9u8RkUoPZPXXuk+eQnlGT133PrkCloDlqP47u/fQ5dV1t6F
+NghgwfOA4QKBgHI/IRMyylBKmj3h6hL4qHqhHiA/Ri7DAHu7hIlrQ4k9ths0wAr/
+IGUzlixC29S8libzBckeX60tm1ez1QuDwaxZZRjVi1V4djERxSoLbchHl5yHoAQv
+JG1Mmnd7I1n6pCefkzn31JfGscUB+sU2sH9+NrUHMqEVb5JfMDRe7p6FAoGAcYGc
+Xqz7gEKkUtSfSyVELxD4dVDtPxuUXsbqmfe1cVA2Q+Pg7NSXKxlZpzak7WEFITVY
+EXtlA8Iu8fnlJuOzpU2BH9VWYi3beseRtew2x2Zksa/JsXkQFekeHiqU3XsWU9WT
+xmw3ldCz+BjMlOvnUAbYNbsIoI4mkQecijKwFkECgYA2zafSyWCW5zAronUBQDEe
+vJumAJ77TwpYzzvH2ic6siWimdePxQ6TgdM3s1FgpdkbaXgKzS5MbZbD0Uyg3MEj
+t6ZT7GSWq39wLDJVDYJ5ClAi8mv9WNs8X8rJ0CkdiPZgHC77OwBELthGn2p9ncar
+Bwhs4S84KEJFT0LAC3YeRQ==
+-----END PRIVATE KEY-----
diff --git a/tests/letstest/testdata/sample-config/live/a.encryption-example.com/README b/tests/letstest/testdata/sample-config/live/a.encryption-example.com/README
new file mode 100644
index 000000000..15194ae3a
--- /dev/null
+++ b/tests/letstest/testdata/sample-config/live/a.encryption-example.com/README
@@ -0,0 +1,10 @@
+This directory contains your keys and certificates.
+
+`privkey.pem` : the private key for your certificate.
+`fullchain.pem`: the certificate file used in most server software.
+`chain.pem` : used for OCSP stapling in Nginx >=1.3.7.
+`cert.pem` : will break many server configurations, and should not be used
+ without reading further documentation (see link below).
+
+We recommend not moving these files. For more information, see the Certbot
+User Guide at https://certbot.eff.org/docs/using.html#where-are-my-certificates.
diff --git a/tests/letstest/testdata/sample-config/live/a.encryption-example.com/cert.pem b/tests/letstest/testdata/sample-config/live/a.encryption-example.com/cert.pem
new file mode 120000
index 000000000..79b6abdf9
--- /dev/null
+++ b/tests/letstest/testdata/sample-config/live/a.encryption-example.com/cert.pem
@@ -0,0 +1 @@
+../../archive/a.encryption-example.com/cert1.pem
\ No newline at end of file
diff --git a/tests/letstest/testdata/sample-config/live/a.encryption-example.com/chain.pem b/tests/letstest/testdata/sample-config/live/a.encryption-example.com/chain.pem
new file mode 120000
index 000000000..2d6b30420
--- /dev/null
+++ b/tests/letstest/testdata/sample-config/live/a.encryption-example.com/chain.pem
@@ -0,0 +1 @@
+../../archive/a.encryption-example.com/chain1.pem
\ No newline at end of file
diff --git a/tests/letstest/testdata/sample-config/live/a.encryption-example.com/fullchain.pem b/tests/letstest/testdata/sample-config/live/a.encryption-example.com/fullchain.pem
new file mode 120000
index 000000000..b801ef735
--- /dev/null
+++ b/tests/letstest/testdata/sample-config/live/a.encryption-example.com/fullchain.pem
@@ -0,0 +1 @@
+../../archive/a.encryption-example.com/fullchain1.pem
\ No newline at end of file
diff --git a/tests/letstest/testdata/sample-config/live/a.encryption-example.com/privkey.pem b/tests/letstest/testdata/sample-config/live/a.encryption-example.com/privkey.pem
new file mode 120000
index 000000000..74e20c5ff
--- /dev/null
+++ b/tests/letstest/testdata/sample-config/live/a.encryption-example.com/privkey.pem
@@ -0,0 +1 @@
+../../archive/a.encryption-example.com/privkey1.pem
\ No newline at end of file
diff --git a/tests/letstest/testdata/sample-config/live/b.encryption-example.com/README b/tests/letstest/testdata/sample-config/live/b.encryption-example.com/README
new file mode 100644
index 000000000..15194ae3a
--- /dev/null
+++ b/tests/letstest/testdata/sample-config/live/b.encryption-example.com/README
@@ -0,0 +1,10 @@
+This directory contains your keys and certificates.
+
+`privkey.pem` : the private key for your certificate.
+`fullchain.pem`: the certificate file used in most server software.
+`chain.pem` : used for OCSP stapling in Nginx >=1.3.7.
+`cert.pem` : will break many server configurations, and should not be used
+ without reading further documentation (see link below).
+
+We recommend not moving these files. For more information, see the Certbot
+User Guide at https://certbot.eff.org/docs/using.html#where-are-my-certificates.
diff --git a/tests/letstest/testdata/sample-config/live/b.encryption-example.com/cert.pem b/tests/letstest/testdata/sample-config/live/b.encryption-example.com/cert.pem
new file mode 120000
index 000000000..41b06370e
--- /dev/null
+++ b/tests/letstest/testdata/sample-config/live/b.encryption-example.com/cert.pem
@@ -0,0 +1 @@
+../../archive/b.encryption-example.com/cert1.pem
\ No newline at end of file
diff --git a/tests/letstest/testdata/sample-config/live/b.encryption-example.com/chain.pem b/tests/letstest/testdata/sample-config/live/b.encryption-example.com/chain.pem
new file mode 120000
index 000000000..2d3e18bec
--- /dev/null
+++ b/tests/letstest/testdata/sample-config/live/b.encryption-example.com/chain.pem
@@ -0,0 +1 @@
+../../archive/b.encryption-example.com/chain1.pem
\ No newline at end of file
diff --git a/tests/letstest/testdata/sample-config/live/b.encryption-example.com/fullchain.pem b/tests/letstest/testdata/sample-config/live/b.encryption-example.com/fullchain.pem
new file mode 120000
index 000000000..3a08c1432
--- /dev/null
+++ b/tests/letstest/testdata/sample-config/live/b.encryption-example.com/fullchain.pem
@@ -0,0 +1 @@
+../../archive/b.encryption-example.com/fullchain1.pem
\ No newline at end of file
diff --git a/tests/letstest/testdata/sample-config/live/b.encryption-example.com/privkey.pem b/tests/letstest/testdata/sample-config/live/b.encryption-example.com/privkey.pem
new file mode 120000
index 000000000..182aa6d78
--- /dev/null
+++ b/tests/letstest/testdata/sample-config/live/b.encryption-example.com/privkey.pem
@@ -0,0 +1 @@
+../../archive/b.encryption-example.com/privkey1.pem
\ No newline at end of file
diff --git a/tests/letstest/testdata/sample-config/options-ssl-apache.conf b/tests/letstest/testdata/sample-config/options-ssl-apache.conf
new file mode 100644
index 000000000..ec07a4ba3
--- /dev/null
+++ b/tests/letstest/testdata/sample-config/options-ssl-apache.conf
@@ -0,0 +1,22 @@
+# Baseline setting to Include for SSL sites
+
+SSLEngine on
+
+# Intermediate configuration, tweak to your needs
+SSLProtocol all -SSLv2 -SSLv3
+SSLCipherSuite ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA
+SSLHonorCipherOrder on
+SSLCompression off
+
+SSLOptions +StrictRequire
+
+# Add vhost name to log entries:
+LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-agent}i\"" vhost_combined
+LogFormat "%v %h %l %u %t \"%r\" %>s %b" vhost_common
+
+#CustomLog /var/log/apache2/access.log vhost_combined
+#LogLevel warn
+#ErrorLog /var/log/apache2/error.log
+
+# Always ensure Cookies have "Secure" set (JAH 2012/1)
+#Header edit Set-Cookie (?i)^(.*)(;\s*secure)??((\s*;)?(.*)) "$1; Secure$3$4"
diff --git a/tests/letstest/testdata/sample-config/renewal/a.encryption-example.com.conf b/tests/letstest/testdata/sample-config/renewal/a.encryption-example.com.conf
new file mode 100644
index 000000000..4455137b4
--- /dev/null
+++ b/tests/letstest/testdata/sample-config/renewal/a.encryption-example.com.conf
@@ -0,0 +1,15 @@
+# renew_before_expiry = 30 days
+version = 0.10.0.dev0
+archive_dir = sample-config/archive/a.encryption-example.com
+cert = sample-config/live/a.encryption-example.com/cert.pem
+privkey = sample-config/live/a.encryption-example.com/privkey.pem
+chain = sample-config/live/a.encryption-example.com/chain.pem
+fullchain = sample-config/live/a.encryption-example.com/fullchain.pem
+
+# Options used in the renewal process
+[renewalparams]
+authenticator = apache
+installer = apache
+account = 48d6b9e8d767eccf7e4d877d6ffa81e3
+config_dir = sample-config
+server = https://acme-staging.api.letsencrypt.org/directory
diff --git a/tests/letstest/testdata/sample-config/renewal/b.encryption-example.com.conf b/tests/letstest/testdata/sample-config/renewal/b.encryption-example.com.conf
new file mode 100644
index 000000000..58d8a13d9
--- /dev/null
+++ b/tests/letstest/testdata/sample-config/renewal/b.encryption-example.com.conf
@@ -0,0 +1,15 @@
+# renew_before_expiry = 30 days
+version = 0.10.0.dev0
+archive_dir = sample-config/archive/b.encryption-example.com
+cert = sample-config/live/b.encryption-example.com/cert.pem
+privkey = sample-config/live/b.encryption-example.com/privkey.pem
+chain = sample-config/live/b.encryption-example.com/chain.pem
+fullchain = sample-config/live/b.encryption-example.com/fullchain.pem
+
+# Options used in the renewal process
+[renewalparams]
+authenticator = apache
+installer = apache
+account = 48d6b9e8d767eccf7e4d877d6ffa81e3
+config_dir = sample-config
+server = https://acme-staging.api.letsencrypt.org/directory
diff --git a/tests/manual-dns-auth.sh b/tests/manual-dns-auth.sh
new file mode 100755
index 000000000..9b9a1a5eb
--- /dev/null
+++ b/tests/manual-dns-auth.sh
@@ -0,0 +1,4 @@
+#!/bin/sh
+curl -X POST 'http://localhost:8055/set-txt' -d \
+ "{\"host\": \"_acme-challenge.$CERTBOT_DOMAIN.\", \
+ \"value\": \"$CERTBOT_VALIDATION\"}"
diff --git a/tests/manual-http-auth.sh b/tests/manual-http-auth.sh
new file mode 100755
index 000000000..c4730392b
--- /dev/null
+++ b/tests/manual-http-auth.sh
@@ -0,0 +1,12 @@
+#!/bin/sh
+uri_path=".well-known/acme-challenge/$CERTBOT_TOKEN"
+
+cd $(mktemp -d)
+mkdir -p $(dirname $uri_path)
+echo $CERTBOT_VALIDATION > $uri_path
+python -m SimpleHTTPServer $http_01_port >/dev/null 2>&1 &
+server_pid=$!
+while ! curl "http://localhost:$http_01_port/$uri_path" >/dev/null 2>&1; do
+ sleep 1s
+done
+echo $server_pid
diff --git a/tests/manual-http-cleanup.sh b/tests/manual-http-cleanup.sh
new file mode 100755
index 000000000..5e437bf08
--- /dev/null
+++ b/tests/manual-http-cleanup.sh
@@ -0,0 +1,2 @@
+#!/bin/sh
+kill $CERTBOT_AUTH_OUTPUT
diff --git a/tests/modification-check.sh b/tests/modification-check.sh
index 53a5efa93..73cdb0c09 100755
--- a/tests/modification-check.sh
+++ b/tests/modification-check.sh
@@ -3,6 +3,13 @@
temp_dir=`mktemp -d`
# Script should be run from Certbot's root directory
+
+SCRIPT_PATH=`dirname $0`
+SCRIPT_PATH=`readlink -f $SCRIPT_PATH`
+FLAG=false
+
+# Compare root letsencrypt-auto and certbot-auto with published versions
+
cp letsencrypt-auto ${temp_dir}/letsencrypt-to-be-checked
cp certbot-auto ${temp_dir}/certbot-to-be-checked
@@ -16,8 +23,7 @@ cmp -s letsencrypt-auto letsencrypt-to-be-checked
if [ $? != 0 ]; then
echo "Root letsencrypt-auto has changed."
- rm -rf temp_dir
- exit 1
+ FLAG=true
else
echo "Root letsencrypt-auto is unchanged."
fi
@@ -26,10 +32,36 @@ cmp -s letsencrypt-auto certbot-to-be-checked
if [ $? != 0 ]; then
echo "Root certbot-auto has changed."
- rm -rf temp_dir
- exit 1
+ FLAG=true
else
echo "Root certbot-auto is unchanged."
fi
+# Cleanup
+rm ${temp_dir}/*
+cd ${SCRIPT_PATH}/../
+
+# Compare letsencrypt-auto-source/letsencrypt-auto with output of build.py
+
+cp letsencrypt-auto-source/letsencrypt-auto ${temp_dir}/original-lea
+python letsencrypt-auto-source/build.py
+cp letsencrypt-auto-source/letsencrypt-auto ${temp_dir}/build-lea
+
+cd $temp_dir
+
+cmp -s original-lea build-lea
+
+if [ $? != 0 ]; then
+ echo "letsencrypt-auto-source/letsencrypt-auto doesn't match output of \
+build.py."
+ FLAG=true
+else
+ echo "letsencrypt-auto-source/letsencrypt-auto matches output of \
+build.py."
+fi
+
+if $FLAG ; then
+ exit 1
+fi
+
rm -rf temp_dir
diff --git a/tools/release.sh b/tools/release.sh
index 57985d7a4..be306d8e0 100755
--- a/tools/release.sh
+++ b/tools/release.sh
@@ -147,6 +147,7 @@ cd ~-
# get a snapshot of the CLI help for the docs
certbot --help all > docs/cli-help.txt
+jws --help > acme/docs/jws-help.txt
cd ..
# freeze before installing anything else, so that we know end-user KGS
@@ -195,7 +196,7 @@ done
# This signature is not quite as strong, but easier for people to verify out of band
gpg -u "$RELEASE_GPG_KEY" --detach-sign --armor --sign letsencrypt-auto-source/letsencrypt-auto
# We can't rename the openssl letsencrypt-auto.sig for compatibility reasons,
-# but we can use the right name for cerbot-auto.asc from day one
+# but we can use the right name for certbot-auto.asc from day one
mv letsencrypt-auto-source/letsencrypt-auto.asc letsencrypt-auto-source/certbot-auto.asc
# copy leauto to the root, overwriting the previous release version
diff --git a/tox.ini b/tox.ini
index ac39e995e..959f44a8d 100644
--- a/tox.ini
+++ b/tox.ini
@@ -4,7 +4,7 @@
[tox]
skipsdist = true
-envlist = py{26,33,34,35},cover,lint
+envlist = modification,py{26,33,34,35},cover,lint
# nosetest -v => more verbose output, allows to detect busy waiting
# loops, especially on Travis
@@ -14,15 +14,15 @@ envlist = py{26,33,34,35},cover,lint
# are detected, c.f. #1002
commands =
pip install -e acme[dns,dev]
- nosetests -v acme
+ nosetests -v acme --processes=-1
pip install -e .[dev]
- nosetests -v certbot
+ nosetests -v certbot --processes=-1 --process-timeout=100
pip install -e certbot-apache
- nosetests -v certbot_apache
+ nosetests -v certbot_apache --processes=-1 --process-timeout=80
pip install -e certbot-nginx
- nosetests -v certbot_nginx
+ nosetests -v certbot_nginx --processes=-1
pip install -e letshelp-certbot
- nosetests -v letshelp_certbot
+ nosetests -v letshelp_certbot --processes=-1
setenv =
PYTHONPATH = {toxinidir}
@@ -31,6 +31,8 @@ setenv =
# 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==0.8
@@ -38,28 +40,33 @@ deps =
py{26,27}-oldest: dnspython>=1.12
py{26,27}-oldest: psutil==2.1.0
py{26,27}-oldest: PyOpenSSL==0.13
- py{26,27}-oldest: python2-pythondialog==3.2.2rc1
+ py{26,27}-oldest: requests<=2.11.1
[testenv:py33]
commands =
pip install -e acme[dns,dev]
- nosetests -v acme
+ nosetests -v acme --processes=-1
pip install -e .[dev]
- nosetests -v certbot
+ nosetests -v certbot --processes=-1 --process-timeout=100
[testenv:py34]
commands =
pip install -e acme[dns,dev]
- nosetests -v acme
+ nosetests -v acme --processes=-1
pip install -e .[dev]
- nosetests -v certbot
+ nosetests -v certbot --processes=-1 --process-timeout=100
[testenv:py35]
commands =
pip install -e acme[dns,dev]
- nosetests -v acme
+ nosetests -v acme --processes=-1
pip install -e .[dev]
- nosetests -v certbot
+ nosetests -v certbot --processes=-1 --process-timeout=100
+
+[testenv:py27_install]
+basepython = python2.7
+commands =
+ pip install -e acme[dns,dev] -e .[dev] -e certbot-apache -e certbot-nginx -e letshelp-certbot
[testenv:cover]
basepython = python2.7
@@ -76,12 +83,8 @@ basepython = python2.7
commands =
pip install -q -e acme[dns,dev] -e .[dev] -e certbot-apache -e certbot-nginx -e certbot-compatibility-test -e letshelp-certbot
./pep8.travis.sh
- pylint --reports=n --rcfile=.pylintrc certbot
pylint --reports=n --rcfile=acme/.pylintrc acme/acme
- pylint --reports=n --rcfile=.pylintrc certbot-apache/certbot_apache
- pylint --reports=n --rcfile=.pylintrc certbot-nginx/certbot_nginx
- pylint --reports=n --rcfile=.pylintrc certbot-compatibility-test/certbot_compatibility_test
- pylint --reports=n --rcfile=.pylintrc letshelp-certbot/letshelp_certbot
+ pylint -j 0 --reports=n --rcfile=.pylintrc certbot certbot-apache/certbot_apache certbot-nginx/certbot_nginx certbot-compatibility-test/certbot_compatibility_test letshelp-certbot/letshelp_certbot
[testenv:apacheconftest]
#basepython = python2.7
@@ -94,16 +97,11 @@ commands =
pip install -e acme[dev] -e .[dev] -e certbot-nginx
python certbot-compatibility-test/nginx/roundtrip.py certbot-compatibility-test/nginx/nginx-roundtrip-testdata
-[testenv:le_auto]
-# At the moment, this tests under Python 2.7 only, as only that version is
-# readily available on the Trusty Docker image.
+# This is a duplication of the command line in testenv:le_auto to
+# allow users to run the modification check by running `tox`
+[testenv:modification]
commands =
{toxinidir}/tests/modification-check.sh
- docker build -t lea letsencrypt-auto-source
- docker run --rm -t -i lea
-whitelist_externals =
- docker
-passenv = DOCKER_*
[testenv:apache_compat]
commands =
@@ -122,3 +120,44 @@ commands =
whitelist_externals =
docker
passenv = DOCKER_*
+
+[testenv:le_auto_precise]
+# At the moment, this tests under Python 2.7 only, as only that version is
+# readily available on the Precise Docker image.
+commands =
+ docker build -f letsencrypt-auto-source/Dockerfile.precise -t lea letsencrypt-auto-source
+ docker run --rm -t -i lea
+whitelist_externals =
+ docker
+passenv = DOCKER_*
+
+[testenv:le_auto_trusty]
+# At the moment, this tests under Python 2.7 only, as only that version is
+# readily available on the Trusty Docker image.
+commands =
+ {toxinidir}/tests/modification-check.sh
+ docker build -f letsencrypt-auto-source/Dockerfile.trusty -t lea letsencrypt-auto-source
+ docker run --rm -t -i lea
+whitelist_externals =
+ docker
+passenv = DOCKER_*
+
+[testenv:le_auto_wheezy]
+# At the moment, this tests under Python 2.7 only, as only that version is
+# readily available on the Wheezy Docker image.
+commands =
+ docker build -f letsencrypt-auto-source/Dockerfile.wheezy -t lea letsencrypt-auto-source
+ docker run --rm -t -i lea
+whitelist_externals =
+ docker
+passenv = DOCKER_*
+
+[testenv:le_auto_centos6]
+# At the moment, this tests under Python 2.6 only, as only that version is
+# readily available on the CentOS 6 Docker image.
+commands =
+ docker build -f letsencrypt-auto-source/Dockerfile.centos6 -t lea letsencrypt-auto-source
+ docker run --rm -t -i lea
+whitelist_externals =
+ docker
+passenv = DOCKER_*