Compare commits
43 Commits
postfix
...
python37-t
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cc85d309a9 | ||
|
|
c1b4f21325 | ||
|
|
29a75eb8a7 | ||
|
|
309a70c3fe | ||
|
|
e5d35099a7 | ||
|
|
9fade9c85c | ||
|
|
cb076539ec | ||
|
|
cad95466b0 | ||
|
|
ab9851d97e | ||
|
|
552e60a126 | ||
|
|
6e13c2ccc7 | ||
|
|
a816cc8979 | ||
|
|
64e06d4201 | ||
|
|
d00a31622d | ||
|
|
742a57722b | ||
|
|
ad3c547e1f | ||
|
|
f169e7374b | ||
|
|
2304f7fcda | ||
|
|
a4760cfe56 | ||
|
|
87e1912bf9 | ||
|
|
80cd134847 | ||
|
|
7890de62ec | ||
|
|
1e1e7d8e97 | ||
|
|
2ac0b55208 | ||
|
|
6771b8e05b | ||
|
|
3877af6619 | ||
|
|
a875246a4b | ||
|
|
40c50231ed | ||
|
|
4ba153949d | ||
|
|
5025b4ea96 | ||
|
|
adc07ef933 | ||
|
|
3316eac178 | ||
|
|
8b16a56de8 | ||
|
|
453eafb11e | ||
|
|
c4ae376279 | ||
|
|
fccfbd14b1 | ||
|
|
c9ae365f66 | ||
|
|
9f20fa0ef9 | ||
|
|
95892cd4ab | ||
|
|
da028ca9c2 | ||
|
|
5bf1c51de7 | ||
|
|
4b11fe1fda | ||
|
|
4ae2390c44 |
@@ -41,7 +41,7 @@ load-plugins=linter_plugin
|
||||
# --enable=similarities". If you want to run only the classes checker, but have
|
||||
# no Warning level messages displayed, use"--disable=all --enable=classes
|
||||
# --disable=W"
|
||||
disable=fixme,locally-disabled,abstract-class-not-used,abstract-class-little-used,bad-continuation,too-few-public-methods,no-self-use,invalid-name,too-many-instance-attributes,cyclic-import,duplicate-code
|
||||
disable=fixme,locally-disabled,locally-enabled,abstract-class-not-used,abstract-class-little-used,bad-continuation,too-few-public-methods,no-self-use,invalid-name,too-many-instance-attributes,cyclic-import,duplicate-code
|
||||
# abstract-class-not-used cannot be disabled locally (at least in
|
||||
# pylint 1.4.1), same for abstract-class-little-used
|
||||
|
||||
|
||||
11
.travis.yml
11
.travis.yml
@@ -41,8 +41,9 @@ matrix:
|
||||
env: TOXENV=py34
|
||||
sudo: required
|
||||
services: docker
|
||||
- python: "3.6"
|
||||
env: TOXENV=py36
|
||||
- python: "3.7"
|
||||
dist: xenial
|
||||
env: TOXENV=py37
|
||||
sudo: required
|
||||
services: docker
|
||||
- sudo: required
|
||||
@@ -77,8 +78,6 @@ sudo: false
|
||||
|
||||
addons:
|
||||
apt:
|
||||
sources:
|
||||
- augeas
|
||||
packages: # Keep in sync with letsencrypt-auto-source/pieces/bootstrappers/deb_common.sh and Boulder.
|
||||
- python-dev
|
||||
- python-virtualenv
|
||||
@@ -90,10 +89,6 @@ addons:
|
||||
# For certbot-nginx integration testing
|
||||
- nginx-light
|
||||
- openssl
|
||||
# for apacheconftest
|
||||
- apache2
|
||||
- libapache2-mod-wsgi
|
||||
- libapache2-mod-macro
|
||||
|
||||
install: "travis_retry $(command -v pip || command -v pip3) install tox coveralls"
|
||||
script:
|
||||
|
||||
22
CHANGELOG.md
22
CHANGELOG.md
@@ -2,6 +2,28 @@
|
||||
|
||||
Certbot adheres to [Semantic Versioning](http://semver.org/).
|
||||
|
||||
## 0.25.1 - 2018-06-13
|
||||
|
||||
### Fixed
|
||||
|
||||
* TLS-ALPN-01 support has been removed from our acme library. Using our current
|
||||
dependencies, we are unable to provide a correct implementation of this
|
||||
challenge so we decided to remove it from the library until we can provide
|
||||
proper support.
|
||||
* Issues causing test failures when running the tests in the acme package with
|
||||
pytest<3.0 has been resolved.
|
||||
* certbot-nginx now correctly depends on acme>=0.25.0.
|
||||
|
||||
Despite us having broken lockstep, we are continuing to release new versions of
|
||||
all Certbot components during releases for the time being, however, the only
|
||||
packages with changes other than their version number were:
|
||||
|
||||
* acme
|
||||
* certbot-nginx
|
||||
|
||||
More details about these changes can be found on our GitHub repo:
|
||||
https://github.com/certbot/certbot/milestone/56?closed=1
|
||||
|
||||
## 0.25.0 - 2018-06-06
|
||||
|
||||
### Added
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
include LICENSE.txt
|
||||
include README.rst
|
||||
include pytest.ini
|
||||
recursive-include docs *
|
||||
recursive-include examples *
|
||||
recursive-include acme/testdata *
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
"""ACME Identifier Validation Challenges."""
|
||||
import abc
|
||||
import codecs
|
||||
import functools
|
||||
import hashlib
|
||||
import logging
|
||||
@@ -8,7 +7,7 @@ import socket
|
||||
|
||||
from cryptography.hazmat.primitives import hashes # type: ignore
|
||||
import josepy as jose
|
||||
from OpenSSL import crypto
|
||||
import OpenSSL
|
||||
import requests
|
||||
import six
|
||||
|
||||
@@ -412,8 +411,8 @@ class TLSSNI01Response(KeyAuthorizationChallengeResponse):
|
||||
|
||||
"""
|
||||
if key is None:
|
||||
key = crypto.PKey()
|
||||
key.generate_key(crypto.TYPE_RSA, bits)
|
||||
key = OpenSSL.crypto.PKey()
|
||||
key.generate_key(OpenSSL.crypto.TYPE_RSA, bits)
|
||||
return crypto_util.gen_ss_cert(key, [
|
||||
# z_domain is too big to fit into CN, hence first dummy domain
|
||||
'dummy', self.z_domain.decode()], force_san=True), key
|
||||
@@ -508,149 +507,19 @@ class TLSSNI01(KeyAuthorizationChallenge):
|
||||
return self.response(account_key).gen_cert(key=kwargs.get('cert_key'))
|
||||
|
||||
|
||||
@ChallengeResponse.register
|
||||
class TLSALPN01Response(KeyAuthorizationChallengeResponse):
|
||||
"""ACME tls-alpn-01 challenge response."""
|
||||
typ = "tls-alpn-01"
|
||||
|
||||
PORT = 443
|
||||
"""Verification port as defined by the protocol.
|
||||
|
||||
You can override it (e.g. for testing) by passing ``port`` to
|
||||
`simple_verify`.
|
||||
|
||||
"""
|
||||
|
||||
ID_PE_ACME_IDENTIFIER_V1 = b"1.3.6.1.5.5.7.1.30.1"
|
||||
ACME_TLS_1_PROTOCOL = "acme-tls/1"
|
||||
|
||||
@property
|
||||
def h(self):
|
||||
"""Hash value stored in challenge certificate"""
|
||||
return hashlib.sha256(self.key_authorization.encode('utf-8')).digest()
|
||||
|
||||
def gen_cert(self, domain, key=None, bits=2048):
|
||||
"""Generate tls-alpn-01 certificate.
|
||||
|
||||
:param unicode domain: Domain verified by the challenge.
|
||||
:param OpenSSL.crypto.PKey key: Optional private key used in
|
||||
certificate generation. If not provided (``None``), then
|
||||
fresh key will be generated.
|
||||
:param int bits: Number of bits for newly generated key.
|
||||
|
||||
:rtype: `tuple` of `OpenSSL.crypto.X509` and `OpenSSL.crypto.PKey`
|
||||
|
||||
"""
|
||||
if key is None:
|
||||
key = crypto.PKey()
|
||||
key.generate_key(crypto.TYPE_RSA, bits)
|
||||
|
||||
|
||||
der_value = b"DER:" + codecs.encode(self.h, 'hex')
|
||||
acme_extension = crypto.X509Extension(self.ID_PE_ACME_IDENTIFIER_V1,
|
||||
critical=True, value=der_value)
|
||||
|
||||
return crypto_util.gen_ss_cert(key, [domain], force_san=True,
|
||||
extensions=[acme_extension]), key
|
||||
|
||||
def probe_cert(self, domain, host=None, port=None):
|
||||
"""Probe tls-alpn-01 challenge certificate.
|
||||
|
||||
:param unicode domain: domain being validated, required.
|
||||
:param string host: IP address used to probe the certificate.
|
||||
:param int port: Port used to probe the certificate.
|
||||
|
||||
"""
|
||||
if host is None:
|
||||
host = socket.gethostbyname(domain)
|
||||
logger.debug('%s resolved to %s', domain, host)
|
||||
if port is None:
|
||||
port = self.PORT
|
||||
|
||||
return crypto_util.probe_sni(host=host, port=port, name=domain,
|
||||
alpn_protocols=[self.ACME_TLS_1_PROTOCOL])
|
||||
|
||||
def verify_cert(self, domain, cert):
|
||||
"""Verify tls-alpn-01 challenge certificate.
|
||||
|
||||
:param unicode domain: Domain name being validated.
|
||||
:param OpensSSL.crypto.X509 cert: Challenge certificate.
|
||||
|
||||
:returns: Whether the certificate was successfully verified.
|
||||
:rtype: bool
|
||||
|
||||
"""
|
||||
# pylint: disable=protected-access
|
||||
names = crypto_util._pyopenssl_cert_or_req_all_names(cert)
|
||||
logger.debug('Certificate %s. SANs: %s', cert.digest('sha256'), names)
|
||||
if len(names) != 1 or names[0].lower() != domain.lower():
|
||||
return False
|
||||
|
||||
for i in range(cert.get_extension_count()):
|
||||
ext = cert.get_extension(i)
|
||||
# FIXME: assume this is the ACME extension. Currently there is no
|
||||
# way to get full OID of an unknown extension from pyopenssl.
|
||||
if ext.get_short_name() == b'UNDEF':
|
||||
data = ext.get_data()
|
||||
return data == self.h
|
||||
|
||||
return False
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
def simple_verify(self, chall, domain, account_public_key,
|
||||
cert=None, host=None, port=None):
|
||||
"""Simple verify.
|
||||
|
||||
Verify ``validation`` using ``account_public_key``, optionally
|
||||
probe tls-alpn-01 certificate and check using `verify_cert`.
|
||||
|
||||
:param .challenges.TLSALPN01 chall: Corresponding challenge.
|
||||
:param str domain: Domain name being validated.
|
||||
:param JWK account_public_key:
|
||||
:param OpenSSL.crypto.X509 cert: Optional certificate. If not
|
||||
provided (``None``) certificate will be retrieved using
|
||||
`probe_cert`.
|
||||
:param string host: IP address used to probe the certificate.
|
||||
:param int port: Port used to probe the certificate.
|
||||
|
||||
|
||||
:returns: ``True`` iff client's control of the domain has been
|
||||
verified.
|
||||
:rtype: bool
|
||||
|
||||
"""
|
||||
if not self.verify(chall, account_public_key):
|
||||
logger.debug("Verification of key authorization in response failed")
|
||||
return False
|
||||
|
||||
if cert is None:
|
||||
try:
|
||||
cert = self.probe_cert(domain=domain, host=host, port=port)
|
||||
except errors.Error as error:
|
||||
logger.debug(str(error), exc_info=True)
|
||||
return False
|
||||
|
||||
return self.verify_cert(cert, domain)
|
||||
|
||||
|
||||
@Challenge.register # pylint: disable=too-many-ancestors
|
||||
class TLSALPN01(KeyAuthorizationChallenge):
|
||||
"""ACME tls-alpn-01 challenge."""
|
||||
response_cls = TLSALPN01Response
|
||||
typ = response_cls.typ
|
||||
"""ACME tls-alpn-01 challenge.
|
||||
|
||||
def validation(self, account_key, **kwargs):
|
||||
"""Generate validation.
|
||||
|
||||
:param JWK account_key:
|
||||
:param OpenSSL.crypto.PKey cert_key: Optional private key used
|
||||
in certificate generation. If not provided (``None``), then
|
||||
fresh key will be generated.
|
||||
|
||||
:rtype: `tuple` of `OpenSSL.crypto.X509` and `OpenSSL.crypto.PKey`
|
||||
This class simply allows parsing the TLS-ALPN-01 challenge returned from
|
||||
the CA. Full TLS-ALPN-01 support is not currently provided.
|
||||
|
||||
"""
|
||||
return self.response(account_key).gen_cert(key=kwargs.get('cert_key'))
|
||||
typ = "tls-alpn-01"
|
||||
|
||||
def validation(self, account_key, **kwargs):
|
||||
"""Generate validation for the challenge."""
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
@Challenge.register # pylint: disable=too-many-ancestors
|
||||
|
||||
@@ -393,91 +393,6 @@ class TLSSNI01Test(unittest.TestCase):
|
||||
mock_gen_cert.assert_called_once_with(key=mock.sentinel.cert_key)
|
||||
|
||||
|
||||
class TLSALPN01ResponseTest(unittest.TestCase):
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
|
||||
def setUp(self):
|
||||
from acme.challenges import TLSALPN01
|
||||
self.chall = TLSALPN01(
|
||||
token=jose.b64decode(b'a82d5ff8ef740d12881f6d3c2277ab2e'))
|
||||
self.domain = u'example.com'
|
||||
self.domain2 = u'example2.com'
|
||||
|
||||
self.response = self.chall.response(KEY)
|
||||
self.jmsg = {
|
||||
'resource': 'challenge',
|
||||
'type': 'tls-alpn-01',
|
||||
'keyAuthorization': self.response.key_authorization,
|
||||
}
|
||||
|
||||
def test_to_partial_json(self):
|
||||
self.assertEqual(self.jmsg, self.response.to_partial_json())
|
||||
|
||||
def test_from_json(self):
|
||||
from acme.challenges import TLSALPN01Response
|
||||
self.assertEqual(self.response, TLSALPN01Response.from_json(self.jmsg))
|
||||
|
||||
def test_from_json_hashable(self):
|
||||
from acme.challenges import TLSALPN01Response
|
||||
hash(TLSALPN01Response.from_json(self.jmsg))
|
||||
|
||||
def test_gen_verify_cert(self):
|
||||
key1 = test_util.load_pyopenssl_private_key('rsa512_key.pem')
|
||||
cert, key2 = self.response.gen_cert(self.domain, key1)
|
||||
self.assertEqual(key1, key2)
|
||||
self.assertTrue(self.response.verify_cert(self.domain, cert))
|
||||
|
||||
def test_gen_verify_cert_gen_key(self):
|
||||
cert, key = self.response.gen_cert(self.domain)
|
||||
self.assertTrue(isinstance(key, OpenSSL.crypto.PKey))
|
||||
self.assertTrue(self.response.verify_cert(self.domain, cert))
|
||||
|
||||
def test_verify_bad_cert(self):
|
||||
self.assertFalse(self.response.verify_cert(self.domain,
|
||||
test_util.load_cert('cert.pem')))
|
||||
|
||||
def test_verify_bad_domain(self):
|
||||
key1 = test_util.load_pyopenssl_private_key('rsa512_key.pem')
|
||||
cert, key2 = self.response.gen_cert(self.domain, key1)
|
||||
self.assertEqual(key1, key2)
|
||||
self.assertFalse(self.response.verify_cert(self.domain2, cert))
|
||||
|
||||
def test_simple_verify_bad_key_authorization(self):
|
||||
key2 = jose.JWKRSA.load(test_util.load_vector('rsa256_key.pem'))
|
||||
self.response.simple_verify(self.chall, "local", key2.public_key())
|
||||
|
||||
@mock.patch('acme.challenges.TLSALPN01Response.verify_cert', autospec=True)
|
||||
def test_simple_verify(self, mock_verify_cert):
|
||||
mock_verify_cert.return_value = mock.sentinel.verification
|
||||
self.assertEqual(
|
||||
mock.sentinel.verification, self.response.simple_verify(
|
||||
self.chall, self.domain, KEY.public_key(),
|
||||
cert=mock.sentinel.cert))
|
||||
mock_verify_cert.assert_called_once_with(
|
||||
self.response, mock.sentinel.cert, self.domain)
|
||||
|
||||
@mock.patch('acme.challenges.socket.gethostbyname')
|
||||
@mock.patch('acme.challenges.crypto_util.probe_sni')
|
||||
def test_probe_cert(self, mock_probe_sni, mock_gethostbyname):
|
||||
mock_gethostbyname.return_value = '127.0.0.1'
|
||||
self.response.probe_cert('foo.com')
|
||||
mock_gethostbyname.assert_called_once_with('foo.com')
|
||||
mock_probe_sni.assert_called_once_with(
|
||||
host='127.0.0.1', port=self.response.PORT, name='foo.com',
|
||||
alpn_protocols=['acme-tls/1'])
|
||||
|
||||
self.response.probe_cert('foo.com', host='8.8.8.8')
|
||||
mock_probe_sni.assert_called_with(
|
||||
host='8.8.8.8', port=mock.ANY, name='foo.com',
|
||||
alpn_protocols=['acme-tls/1'])
|
||||
|
||||
@mock.patch('acme.challenges.TLSALPN01Response.probe_cert')
|
||||
def test_simple_verify_false_on_probe_error(self, mock_probe_cert):
|
||||
mock_probe_cert.side_effect = errors.Error
|
||||
self.assertFalse(self.response.simple_verify(
|
||||
self.chall, self.domain, KEY.public_key()))
|
||||
|
||||
|
||||
class TLSALPN01Test(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
@@ -506,12 +421,8 @@ class TLSALPN01Test(unittest.TestCase):
|
||||
self.assertRaises(
|
||||
jose.DeserializationError, TLSALPN01.from_json, self.jmsg)
|
||||
|
||||
@mock.patch('acme.challenges.TLSALPN01Response.gen_cert')
|
||||
def test_validation(self, mock_gen_cert):
|
||||
mock_gen_cert.return_value = ('cert', 'key')
|
||||
self.assertEqual(('cert', 'key'), self.msg.validation(
|
||||
KEY, cert_key=mock.sentinel.cert_key))
|
||||
mock_gen_cert.assert_called_once_with(key=mock.sentinel.cert_key)
|
||||
def test_validation(self):
|
||||
self.assertRaises(NotImplementedError, self.msg.validation, KEY)
|
||||
|
||||
|
||||
class DNSTest(unittest.TestCase):
|
||||
|
||||
@@ -31,15 +31,6 @@ logger = logging.getLogger(__name__)
|
||||
_DEFAULT_TLSSNI01_SSL_METHOD = SSL.SSLv23_METHOD # type: ignore
|
||||
|
||||
|
||||
class _DefaultCertSelection(object):
|
||||
def __init__(self, certs):
|
||||
self.certs = certs
|
||||
|
||||
def __call__(self, connection):
|
||||
server_name = connection.get_servername()
|
||||
return self.certs.get(server_name, None)
|
||||
|
||||
|
||||
class SSLSocket(object): # pylint: disable=too-few-public-methods
|
||||
"""SSL wrapper for sockets.
|
||||
|
||||
@@ -47,25 +38,12 @@ class SSLSocket(object): # pylint: disable=too-few-public-methods
|
||||
:ivar dict certs: Mapping from domain names (`bytes`) to
|
||||
`OpenSSL.crypto.X509`.
|
||||
:ivar method: See `OpenSSL.SSL.Context` for allowed values.
|
||||
:ivar alpn_selection: Hook to select negotiated ALPN protocol for
|
||||
connection.
|
||||
:ivar cert_selection: Hook to select certificate for connection. If given,
|
||||
`certs` parameter would be ignored, and therefore must be empty.
|
||||
|
||||
"""
|
||||
def __init__(self, sock, certs=None,
|
||||
method=_DEFAULT_TLSSNI01_SSL_METHOD, alpn_selection=None,
|
||||
cert_selection=None):
|
||||
def __init__(self, sock, certs, method=_DEFAULT_TLSSNI01_SSL_METHOD):
|
||||
self.sock = sock
|
||||
self.alpn_selection = alpn_selection
|
||||
self.certs = certs
|
||||
self.method = method
|
||||
if not cert_selection and not certs:
|
||||
raise ValueError("Neither cert_selection or certs specified.")
|
||||
if cert_selection and certs:
|
||||
raise ValueError("Both cert_selection and certs specified.")
|
||||
if cert_selection is None:
|
||||
cert_selection = _DefaultCertSelection(certs)
|
||||
self.cert_selection = cert_selection
|
||||
|
||||
def __getattr__(self, name):
|
||||
return getattr(self.sock, name)
|
||||
@@ -82,19 +60,18 @@ class SSLSocket(object): # pylint: disable=too-few-public-methods
|
||||
:type connection: :class:`OpenSSL.Connection`
|
||||
|
||||
"""
|
||||
pair = self.cert_selection(connection)
|
||||
if pair is None:
|
||||
logger.debug("Certificate selection for server name %s failed, dropping SSL",
|
||||
connection.get_servername())
|
||||
server_name = connection.get_servername()
|
||||
try:
|
||||
key, cert = self.certs[server_name]
|
||||
except KeyError:
|
||||
logger.debug("Server name (%s) not recognized, dropping SSL",
|
||||
server_name)
|
||||
return
|
||||
key, cert = pair
|
||||
new_context = SSL.Context(self.method)
|
||||
new_context.set_options(SSL.OP_NO_SSLv2)
|
||||
new_context.set_options(SSL.OP_NO_SSLv3)
|
||||
new_context.use_privatekey(key)
|
||||
new_context.use_certificate(cert)
|
||||
if self.alpn_selection is not None:
|
||||
new_context.set_alpn_select_callback(self.alpn_selection)
|
||||
connection.set_context(new_context)
|
||||
|
||||
class FakeConnection(object):
|
||||
@@ -119,8 +96,6 @@ class SSLSocket(object): # pylint: disable=too-few-public-methods
|
||||
context.set_options(SSL.OP_NO_SSLv2)
|
||||
context.set_options(SSL.OP_NO_SSLv3)
|
||||
context.set_tlsext_servername_callback(self._pick_certificate_cb)
|
||||
if self.alpn_selection is not None:
|
||||
context.set_alpn_select_callback(self.alpn_selection)
|
||||
|
||||
ssl_sock = self.FakeConnection(SSL.Connection(context, sock))
|
||||
ssl_sock.set_accept_state()
|
||||
@@ -136,9 +111,8 @@ class SSLSocket(object): # pylint: disable=too-few-public-methods
|
||||
return ssl_sock, addr
|
||||
|
||||
|
||||
def probe_sni(name, host, port=443, timeout=300, # pylint: disable=too-many-arguments
|
||||
method=_DEFAULT_TLSSNI01_SSL_METHOD, source_address=('', 0),
|
||||
alpn_protocols=None):
|
||||
def probe_sni(name, host, port=443, timeout=300,
|
||||
method=_DEFAULT_TLSSNI01_SSL_METHOD, source_address=('', 0)):
|
||||
"""Probe SNI server for SSL certificate.
|
||||
|
||||
:param bytes name: Byte string to send as the server name in the
|
||||
@@ -150,8 +124,6 @@ def probe_sni(name, host, port=443, timeout=300, # pylint: disable=too-many-argu
|
||||
:param tuple source_address: Enables multi-path probing (selection
|
||||
of source interface). See `socket.creation_connection` for more
|
||||
info. Available only in Python 2.7+.
|
||||
:param alpn_protocols: Protocols to request using ALPN.
|
||||
:type alpn_protocols: `list` of `bytes`
|
||||
|
||||
:raises acme.errors.Error: In case of any problems.
|
||||
|
||||
@@ -188,8 +160,6 @@ def probe_sni(name, host, port=443, timeout=300, # pylint: disable=too-many-argu
|
||||
client_ssl = SSL.Connection(context, client)
|
||||
client_ssl.set_connect_state()
|
||||
client_ssl.set_tlsext_host_name(name) # pyOpenSSL>=0.13
|
||||
if alpn_protocols is not None:
|
||||
client_ssl.set_alpn_protos(alpn_protocols)
|
||||
try:
|
||||
client_ssl.do_handshake()
|
||||
client_ssl.shutdown()
|
||||
@@ -281,14 +251,12 @@ def _pyopenssl_cert_or_req_san(cert_or_req):
|
||||
|
||||
|
||||
def gen_ss_cert(key, domains, not_before=None,
|
||||
validity=(7 * 24 * 60 * 60), force_san=True, extensions=None):
|
||||
validity=(7 * 24 * 60 * 60), force_san=True):
|
||||
"""Generate new self-signed certificate.
|
||||
|
||||
:type domains: `list` of `unicode`
|
||||
:param OpenSSL.crypto.PKey key:
|
||||
:param bool force_san:
|
||||
:param extensions: List of additional extensions to include in the cert.
|
||||
:type extensions: `list` of `OpenSSL.crypto.X509Extension`
|
||||
|
||||
If more than one domain is provided, all of the domains are put into
|
||||
``subjectAltName`` X.509 extension and first domain is set as the
|
||||
@@ -301,13 +269,10 @@ def gen_ss_cert(key, domains, not_before=None,
|
||||
cert.set_serial_number(int(binascii.hexlify(os.urandom(16)), 16))
|
||||
cert.set_version(2)
|
||||
|
||||
if extensions is None:
|
||||
extensions = []
|
||||
|
||||
extensions.append(
|
||||
extensions = [
|
||||
crypto.X509Extension(
|
||||
b"basicConstraints", True, b"CA:TRUE, pathlen:0"),
|
||||
)
|
||||
]
|
||||
|
||||
cert.get_subject().CN = domains[0]
|
||||
# TODO: what to put into cert.get_subject()?
|
||||
|
||||
@@ -19,6 +19,7 @@ from acme.magic_typing import List # pylint: disable=unused-import, no-name-in-m
|
||||
class SSLSocketAndProbeSNITest(unittest.TestCase):
|
||||
"""Tests for acme.crypto_util.SSLSocket/probe_sni."""
|
||||
|
||||
|
||||
def setUp(self):
|
||||
self.cert = test_util.load_comparable_cert('rsa2048_cert.pem')
|
||||
key = test_util.load_pyopenssl_private_key('rsa2048_key.pem')
|
||||
@@ -33,8 +34,7 @@ class SSLSocketAndProbeSNITest(unittest.TestCase):
|
||||
# six.moves.* | pylint: disable=attribute-defined-outside-init,no-init
|
||||
|
||||
def server_bind(self): # pylint: disable=missing-docstring
|
||||
self.socket = SSLSocket(socket.socket(),
|
||||
certs)
|
||||
self.socket = SSLSocket(socket.socket(), certs=certs)
|
||||
socketserver.TCPServer.server_bind(self)
|
||||
|
||||
self.server = _TestServer(('', 0), socketserver.BaseRequestHandler)
|
||||
@@ -42,40 +42,38 @@ class SSLSocketAndProbeSNITest(unittest.TestCase):
|
||||
self.server_thread = threading.Thread(
|
||||
# pylint: disable=no-member
|
||||
target=self.server.handle_request)
|
||||
self.server_thread.start()
|
||||
time.sleep(1) # TODO: avoid race conditions in other way
|
||||
|
||||
def tearDown(self):
|
||||
self.server_thread.join()
|
||||
if self.server_thread.is_alive():
|
||||
# The thread may have already terminated.
|
||||
self.server_thread.join() # pragma: no cover
|
||||
|
||||
def _probe(self, name):
|
||||
from acme.crypto_util import probe_sni
|
||||
return jose.ComparableX509(probe_sni(
|
||||
name, host='127.0.0.1', port=self.port))
|
||||
|
||||
def _start_server(self):
|
||||
self.server_thread.start()
|
||||
time.sleep(1) # TODO: avoid race conditions in other way
|
||||
|
||||
def test_probe_ok(self):
|
||||
self._start_server()
|
||||
self.assertEqual(self.cert, self._probe(b'foo'))
|
||||
|
||||
def test_probe_not_recognized_name(self):
|
||||
self._start_server()
|
||||
self.assertRaises(errors.Error, self._probe, b'bar')
|
||||
|
||||
# TODO: py33/py34 tox hangs forever on do_handshake in second probe
|
||||
#def probe_connection_error(self):
|
||||
# self._probe(b'foo')
|
||||
# #time.sleep(1) # TODO: avoid race conditions in other way
|
||||
# self.assertRaises(errors.Error, self._probe, b'bar')
|
||||
|
||||
|
||||
class SSLSocketTest(unittest.TestCase):
|
||||
"""Tests for acme.crypto_util.SSLSocket."""
|
||||
|
||||
def test_ssl_socket_invalid_arguments(self):
|
||||
from acme.crypto_util import SSLSocket
|
||||
with self.assertRaises(ValueError):
|
||||
_ = SSLSocket(None, {'sni': ('key', 'cert')},
|
||||
cert_selection=lambda _: None)
|
||||
with self.assertRaises(ValueError):
|
||||
_ = SSLSocket(None)
|
||||
def test_probe_connection_error(self):
|
||||
# pylint has a hard time with six
|
||||
self.server.server_close() # pylint: disable=no-member
|
||||
original_timeout = socket.getdefaulttimeout()
|
||||
try:
|
||||
socket.setdefaulttimeout(1)
|
||||
self.assertRaises(errors.Error, self._probe, b'bar')
|
||||
finally:
|
||||
socket.setdefaulttimeout(original_timeout)
|
||||
|
||||
|
||||
class PyOpenSSLCertOrReqAllNamesTest(unittest.TestCase):
|
||||
|
||||
@@ -43,14 +43,7 @@ class TLSServer(socketserver.TCPServer):
|
||||
|
||||
def _wrap_sock(self):
|
||||
self.socket = crypto_util.SSLSocket(
|
||||
self.socket, cert_selection=self._cert_selection,
|
||||
alpn_selection=getattr(self, '_alpn_selection', None),
|
||||
method=self.method)
|
||||
|
||||
def _cert_selection(self, connection):
|
||||
"""Callback selecting certificate for connection."""
|
||||
server_name = connection.get_servername()
|
||||
return self.certs.get(server_name, None)
|
||||
self.socket, certs=self.certs, method=self.method)
|
||||
|
||||
def server_bind(self): # pylint: disable=missing-docstring
|
||||
self._wrap_sock()
|
||||
@@ -154,45 +147,6 @@ class TLSSNI01DualNetworkedServers(BaseDualNetworkedServers):
|
||||
BaseDualNetworkedServers.__init__(self, TLSSNI01Server, *args, **kwargs)
|
||||
|
||||
|
||||
class BadALPNProtos(Exception):
|
||||
"""Error raised when cannot negotiate ALPN protocol."""
|
||||
pass
|
||||
|
||||
|
||||
class TLSALPN01Server(TLSServer, ACMEServerMixin):
|
||||
"""TLSALPN01 Server."""
|
||||
|
||||
ACME_TLS_1_PROTOCOL = b"acme-tls/1"
|
||||
|
||||
def __init__(self, server_address, certs, challenge_certs, ipv6=False):
|
||||
TLSServer.__init__(
|
||||
self, server_address, BaseRequestHandlerWithLogging, certs=certs,
|
||||
ipv6=ipv6)
|
||||
self.challenge_certs = challenge_certs
|
||||
|
||||
def _cert_selection(self, connection):
|
||||
# TODO: We would like to serve challenge cert only if asked for it via
|
||||
# ALPN. To do this, we need to retrieve the list of protos from client
|
||||
# hello, but this is currently impossible with openssl [0], and ALPN
|
||||
# negotiation is done after cert selection.
|
||||
# Therefore, currently we always return challenge cert, and terminate
|
||||
# handshake in alpn_selection() if ALPN protos are not what we expect.
|
||||
# [0] https://github.com/openssl/openssl/issues/4952
|
||||
server_name = connection.get_servername()
|
||||
logger.debug("Serving challenge cert for server name %s", server_name)
|
||||
return self.challenge_certs.get(server_name, None)
|
||||
|
||||
def _alpn_selection(self, _connection, alpn_protos):
|
||||
"""Callback to select alpn protocol."""
|
||||
if len(alpn_protos) == 1 and alpn_protos[0] == self.ACME_TLS_1_PROTOCOL:
|
||||
logger.debug("Agreed on %s ALPN", self.ACME_TLS_1_PROTOCOL)
|
||||
return self.ACME_TLS_1_PROTOCOL
|
||||
# Raising an exception causes openssl to terminate handshake and
|
||||
# send fatal tls alert.
|
||||
logger.debug("Cannot agree on ALPN proto. Got: %s", str(alpn_protos))
|
||||
raise BadALPNProtos("Got: %s" % str(alpn_protos))
|
||||
|
||||
|
||||
class BaseRequestHandlerWithLogging(socketserver.BaseRequestHandler):
|
||||
"""BaseRequestHandler with logging."""
|
||||
|
||||
|
||||
@@ -4,20 +4,18 @@ import shutil
|
||||
import socket
|
||||
import threading
|
||||
import tempfile
|
||||
import time
|
||||
import unittest
|
||||
|
||||
from six.moves import http_client # pylint: disable=import-error
|
||||
from six.moves import queue # pylint: disable=import-error
|
||||
from six.moves import socketserver # type: ignore # pylint: disable=import-error
|
||||
|
||||
from OpenSSL import SSL # type: ignore # https://github.com/python/typeshed/issues/2052
|
||||
import josepy as jose
|
||||
import mock
|
||||
import requests
|
||||
|
||||
from acme import challenges
|
||||
from acme import crypto_util
|
||||
from acme import errors
|
||||
from acme import test_util
|
||||
from acme.magic_typing import Set # pylint: disable=unused-import, no-name-in-module
|
||||
|
||||
@@ -120,62 +118,6 @@ class HTTP01ServerTest(unittest.TestCase):
|
||||
self.assertFalse(self._test_http01(add=False))
|
||||
|
||||
|
||||
@unittest.skipUnless(
|
||||
hasattr(SSL.Connection, "set_alpn_protos") and
|
||||
hasattr(SSL.Context, "set_alpn_select_callback"),
|
||||
"pyOpenSSL too old")
|
||||
class TLSALPN01ServerTest(unittest.TestCase):
|
||||
"""Test for acme.standalone.TLSALPN01Server."""
|
||||
|
||||
def setUp(self):
|
||||
self.certs = {b'localhost': (
|
||||
test_util.load_pyopenssl_private_key('rsa2048_key.pem'),
|
||||
test_util.load_cert('rsa2048_cert.pem'),
|
||||
)}
|
||||
# Use different certificate for challenge.
|
||||
self.challenge_certs = {b'localhost': (
|
||||
test_util.load_pyopenssl_private_key('rsa1024_key.pem'),
|
||||
test_util.load_cert('rsa1024_cert.pem'),
|
||||
)}
|
||||
from acme.standalone import TLSALPN01Server
|
||||
self.server = TLSALPN01Server(("", 0), certs=self.certs,
|
||||
challenge_certs=self.challenge_certs)
|
||||
# pylint: disable=no-member
|
||||
self.thread = threading.Thread(target=self.server.serve_forever)
|
||||
self.thread.start()
|
||||
|
||||
def tearDown(self):
|
||||
self.server.shutdown() # pylint: disable=no-member
|
||||
self.thread.join()
|
||||
|
||||
#TODO: This is not implemented yet, see comments in standalone.py
|
||||
#def test_certs(self):
|
||||
# host, port = self.server.socket.getsockname()[:2]
|
||||
# cert = crypto_util.probe_sni(
|
||||
# b'localhost', host=host, port=port, timeout=1)
|
||||
# # Expect normal cert when connecting without ALPN.
|
||||
# self.assertEqual(jose.ComparableX509(cert),
|
||||
# jose.ComparableX509(self.certs[b'localhost'][1]))
|
||||
|
||||
def test_challenge_certs(self):
|
||||
host, port = self.server.socket.getsockname()[:2]
|
||||
cert = crypto_util.probe_sni(
|
||||
b'localhost', host=host, port=port, timeout=1,
|
||||
alpn_protocols=[b"acme-tls/1"])
|
||||
# Expect challenge cert when connecting with ALPN.
|
||||
self.assertEqual(
|
||||
jose.ComparableX509(cert),
|
||||
jose.ComparableX509(self.challenge_certs[b'localhost'][1])
|
||||
)
|
||||
|
||||
def test_bad_alpn(self):
|
||||
host, port = self.server.socket.getsockname()[:2]
|
||||
with self.assertRaises(errors.Error):
|
||||
crypto_util.probe_sni(
|
||||
b'localhost', host=host, port=port, timeout=1,
|
||||
alpn_protocols=[b"bad-alpn"])
|
||||
|
||||
|
||||
class BaseDualNetworkedServersTest(unittest.TestCase):
|
||||
"""Test for acme.standalone.BaseDualNetworkedServers."""
|
||||
|
||||
@@ -318,10 +260,9 @@ class TestSimpleTLSSNI01Server(unittest.TestCase):
|
||||
os.path.join(localhost_dir, 'key.pem'))
|
||||
|
||||
from acme.standalone import simple_tls_sni_01_server
|
||||
self.port = 1234
|
||||
self.thread = threading.Thread(
|
||||
target=simple_tls_sni_01_server, kwargs={
|
||||
'cli_args': ('xxx', '--port', str(self.port)),
|
||||
'cli_args': ('filename',),
|
||||
'forever': False,
|
||||
},
|
||||
)
|
||||
@@ -333,25 +274,20 @@ class TestSimpleTLSSNI01Server(unittest.TestCase):
|
||||
self.thread.join()
|
||||
shutil.rmtree(self.test_cwd)
|
||||
|
||||
def test_it(self):
|
||||
max_attempts = 5
|
||||
for attempt in range(max_attempts):
|
||||
try:
|
||||
cert = crypto_util.probe_sni(
|
||||
b'localhost', b'0.0.0.0', self.port)
|
||||
except errors.Error:
|
||||
self.assertTrue(attempt + 1 < max_attempts, "Timeout!")
|
||||
time.sleep(1) # wait until thread starts
|
||||
else:
|
||||
@mock.patch('acme.standalone.logger')
|
||||
def test_it(self, mock_logger):
|
||||
# Use a Queue because mock objects aren't thread safe.
|
||||
q = queue.Queue() # type: queue.Queue[int]
|
||||
# Add port number to the queue.
|
||||
mock_logger.info.side_effect = lambda *args: q.put(args[-1])
|
||||
self.thread.start()
|
||||
|
||||
# After the timeout, an exception is raised if the queue is empty.
|
||||
port = q.get(timeout=5)
|
||||
cert = crypto_util.probe_sni(b'localhost', b'0.0.0.0', port)
|
||||
self.assertEqual(jose.ComparableX509(cert),
|
||||
test_util.load_comparable_cert(
|
||||
'rsa2048_cert.pem'))
|
||||
break
|
||||
|
||||
if attempt == 0:
|
||||
# the first attempt is always meant to fail, so we can test
|
||||
# the socket failure code-path for probe_sni, as well
|
||||
self.thread.start()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
6
acme/acme/testdata/README
vendored
6
acme/acme/testdata/README
vendored
@@ -10,8 +10,6 @@ and for the CSR:
|
||||
|
||||
openssl req -key rsa2048_key.pem -new -subj '/CN=example.com' -outform DER > csr.der
|
||||
|
||||
and for the certificates:
|
||||
and for the certificate:
|
||||
|
||||
openssl req -key rsa2048_key.pem -new -subj '/CN=example.com' -x509 -outform DER > cert.der
|
||||
openssl req -key rsa2048_key.pem -new -subj '/CN=example.com' -x509 > rsa2048_cert.pem
|
||||
openssl req -key rsa1024_key.pem -new -subj '/CN=example.com' -x509 > rsa1024_cert.pem
|
||||
openssl req -key rsa2047_key.pem -new -subj '/CN=example.com' -x509 -outform DER > cert.der
|
||||
|
||||
13
acme/acme/testdata/rsa1024_cert.pem
vendored
13
acme/acme/testdata/rsa1024_cert.pem
vendored
@@ -1,13 +0,0 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIB/TCCAWagAwIBAgIJAOyRIBs3QT8QMA0GCSqGSIb3DQEBCwUAMBYxFDASBgNV
|
||||
BAMMC2V4YW1wbGUuY29tMB4XDTE4MDQyMzEwMzE0NFoXDTE4MDUyMzEwMzE0NFow
|
||||
FjEUMBIGA1UEAwwLZXhhbXBsZS5jb20wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJ
|
||||
AoGBAJqJ87R8aVwByONxgQA9hwgvQd/QqI1r1UInXhEF2VnEtZGtUWLi100IpIqr
|
||||
Mq4qusDwNZ3g8cUPtSkvJGs89djoajMDIJP7lQUEKUYnYrI0q755Tr/DgLWSk7iW
|
||||
l5ezym0VzWUD0/xXUz8yRbNMTjTac80rS5SZk2ja2wWkYlRJAgMBAAGjUzBRMB0G
|
||||
A1UdDgQWBBSsaX0IVZ4XXwdeffVAbG7gnxSYjTAfBgNVHSMEGDAWgBSsaX0IVZ4X
|
||||
XwdeffVAbG7gnxSYjTAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4GB
|
||||
ADe7SVmvGH2nkwVfONk8TauRUDkePN1CJZKFb2zW1uO9ANJ2v5Arm/OQp0BG/xnI
|
||||
Djw/aLTNVESF89oe15dkrUErtcaF413MC1Ld5lTCaJLHLGqDKY69e02YwRuxW7jY
|
||||
qarpt7k7aR5FbcfO5r4V/FK/Gvp4Dmoky8uap7SJIW6x
|
||||
-----END CERTIFICATE-----
|
||||
2
acme/pytest.ini
Normal file
2
acme/pytest.ini
Normal file
@@ -0,0 +1,2 @@
|
||||
[pytest]
|
||||
norecursedirs = .* build dist CVS _darcs {arch} *.egg
|
||||
@@ -3,7 +3,7 @@ from setuptools import find_packages
|
||||
from setuptools.command.test import test as TestCommand
|
||||
import sys
|
||||
|
||||
version = '0.25.0.dev0'
|
||||
version = '0.26.0.dev0'
|
||||
|
||||
# Please update tox.ini when modifying dependency version requirements
|
||||
install_requires = [
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
""" Utility functions for certbot-apache plugin """
|
||||
import binascii
|
||||
import os
|
||||
|
||||
from certbot import util
|
||||
@@ -98,3 +99,8 @@ def parse_define_file(filepath, varname):
|
||||
var_parts = v[2:].partition("=")
|
||||
return_vars[var_parts[0]] = var_parts[2]
|
||||
return return_vars
|
||||
|
||||
|
||||
def unique_id():
|
||||
""" Returns an unique id to be used as a VirtualHost identifier"""
|
||||
return binascii.hexlify(os.urandom(16)).decode("utf-8")
|
||||
|
||||
@@ -13,7 +13,7 @@ import zope.component
|
||||
import zope.interface
|
||||
|
||||
from acme import challenges
|
||||
from acme.magic_typing import DefaultDict, Dict, List, Set # pylint: disable=unused-import, no-name-in-module
|
||||
from acme.magic_typing import Any, DefaultDict, Dict, List, Set, Union # pylint: disable=unused-import, no-name-in-module
|
||||
|
||||
from certbot import errors
|
||||
from certbot import interfaces
|
||||
@@ -22,6 +22,7 @@ from certbot import util
|
||||
from certbot.achallenges import KeyAuthorizationAnnotatedChallenge # pylint: disable=unused-import
|
||||
from certbot.plugins import common
|
||||
from certbot.plugins.util import path_surgery
|
||||
from certbot.plugins.enhancements import AutoHSTSEnhancement
|
||||
|
||||
from certbot_apache import apache_util
|
||||
from certbot_apache import augeas_configurator
|
||||
@@ -160,6 +161,8 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
||||
self._wildcard_vhosts = dict() # type: Dict[str, List[obj.VirtualHost]]
|
||||
# Maps enhancements to vhosts we've enabled the enhancement for
|
||||
self._enhanced_vhosts = defaultdict(set) # type: DefaultDict[str, Set[obj.VirtualHost]]
|
||||
# Temporary state for AutoHSTS enhancement
|
||||
self._autohsts = {} # type: Dict[str, Dict[str, Union[int, float]]]
|
||||
|
||||
# These will be set in the prepare function
|
||||
self.parser = None
|
||||
@@ -1472,6 +1475,67 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
||||
if need_to_save:
|
||||
self.save()
|
||||
|
||||
def find_vhost_by_id(self, id_str):
|
||||
"""
|
||||
Searches through VirtualHosts and tries to match the id in a comment
|
||||
|
||||
:param str id_str: Id string for matching
|
||||
|
||||
:returns: The matched VirtualHost or None
|
||||
:rtype: :class:`~certbot_apache.obj.VirtualHost` or None
|
||||
|
||||
:raises .errors.PluginError: If no VirtualHost is found
|
||||
"""
|
||||
|
||||
for vh in self.vhosts:
|
||||
if self._find_vhost_id(vh) == id_str:
|
||||
return vh
|
||||
msg = "No VirtualHost with ID {} was found.".format(id_str)
|
||||
logger.warning(msg)
|
||||
raise errors.PluginError(msg)
|
||||
|
||||
def _find_vhost_id(self, vhost):
|
||||
"""Tries to find the unique ID from the VirtualHost comments. This is
|
||||
used for keeping track of VirtualHost directive over time.
|
||||
|
||||
:param vhost: Virtual host to add the id
|
||||
:type vhost: :class:`~certbot_apache.obj.VirtualHost`
|
||||
|
||||
:returns: The unique ID or None
|
||||
:rtype: str or None
|
||||
"""
|
||||
|
||||
# Strip the {} off from the format string
|
||||
search_comment = constants.MANAGED_COMMENT_ID.format("")
|
||||
|
||||
id_comment = self.parser.find_comments(search_comment, vhost.path)
|
||||
if id_comment:
|
||||
# Use the first value, multiple ones shouldn't exist
|
||||
comment = self.parser.get_arg(id_comment[0])
|
||||
return comment.split(" ")[-1]
|
||||
return None
|
||||
|
||||
def add_vhost_id(self, vhost):
|
||||
"""Adds an unique ID to the VirtualHost as a comment for mapping back
|
||||
to it on later invocations, as the config file order might have changed.
|
||||
If ID already exists, returns that instead.
|
||||
|
||||
:param vhost: Virtual host to add or find the id
|
||||
:type vhost: :class:`~certbot_apache.obj.VirtualHost`
|
||||
|
||||
:returns: The unique ID for vhost
|
||||
:rtype: str or None
|
||||
"""
|
||||
|
||||
vh_id = self._find_vhost_id(vhost)
|
||||
if vh_id:
|
||||
return vh_id
|
||||
|
||||
id_string = apache_util.unique_id()
|
||||
comment = constants.MANAGED_COMMENT_ID.format(id_string)
|
||||
self.parser.add_comment(vhost.path, comment)
|
||||
return id_string
|
||||
|
||||
def _escape(self, fp):
|
||||
fp = fp.replace(",", "\\,")
|
||||
fp = fp.replace("[", "\\[")
|
||||
@@ -1531,6 +1595,78 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
||||
logger.warning("Failed %s for %s", enhancement, domain)
|
||||
raise
|
||||
|
||||
def _autohsts_increase(self, vhost, id_str, nextstep):
|
||||
"""Increase the AutoHSTS max-age value
|
||||
|
||||
:param vhost: Virtual host object to modify
|
||||
:type vhost: :class:`~certbot_apache.obj.VirtualHost`
|
||||
|
||||
:param str id_str: The unique ID string of VirtualHost
|
||||
|
||||
:param int nextstep: Next AutoHSTS max-age value index
|
||||
|
||||
"""
|
||||
nextstep_value = constants.AUTOHSTS_STEPS[nextstep]
|
||||
self._autohsts_write(vhost, nextstep_value)
|
||||
self._autohsts[id_str] = {"laststep": nextstep, "timestamp": time.time()}
|
||||
|
||||
def _autohsts_write(self, vhost, nextstep_value):
|
||||
"""
|
||||
Write the new HSTS max-age value to the VirtualHost file
|
||||
"""
|
||||
|
||||
hsts_dirpath = None
|
||||
header_path = self.parser.find_dir("Header", None, vhost.path)
|
||||
if header_path:
|
||||
pat = '(?:[ "]|^)(strict-transport-security)(?:[ "]|$)'
|
||||
for match in header_path:
|
||||
if re.search(pat, self.aug.get(match).lower()):
|
||||
hsts_dirpath = match
|
||||
if not hsts_dirpath:
|
||||
err_msg = ("Certbot was unable to find the existing HSTS header "
|
||||
"from the VirtualHost at path {0}.").format(vhost.filep)
|
||||
raise errors.PluginError(err_msg)
|
||||
|
||||
# Prepare the HSTS header value
|
||||
hsts_maxage = "\"max-age={0}\"".format(nextstep_value)
|
||||
|
||||
# Update the header
|
||||
# Our match statement was for string strict-transport-security, but
|
||||
# we need to update the value instead. The next index is for the value
|
||||
hsts_dirpath = hsts_dirpath.replace("arg[3]", "arg[4]")
|
||||
self.aug.set(hsts_dirpath, hsts_maxage)
|
||||
note_msg = ("Increasing HSTS max-age value to {0} for VirtualHost "
|
||||
"in {1}\n".format(nextstep_value, vhost.filep))
|
||||
logger.debug(note_msg)
|
||||
self.save_notes += note_msg
|
||||
self.save(note_msg)
|
||||
|
||||
def _autohsts_fetch_state(self):
|
||||
"""
|
||||
Populates the AutoHSTS state from the pluginstorage
|
||||
"""
|
||||
try:
|
||||
self._autohsts = self.storage.fetch("autohsts")
|
||||
except KeyError:
|
||||
self._autohsts = dict()
|
||||
|
||||
def _autohsts_save_state(self):
|
||||
"""
|
||||
Saves the state of AutoHSTS object to pluginstorage
|
||||
"""
|
||||
self.storage.put("autohsts", self._autohsts)
|
||||
self.storage.save()
|
||||
|
||||
def _autohsts_vhost_in_lineage(self, vhost, lineage):
|
||||
"""
|
||||
Searches AutoHSTS managed VirtualHosts that belong to the lineage.
|
||||
Matches the private key path.
|
||||
"""
|
||||
|
||||
return bool(
|
||||
self.parser.find_dir("SSLCertificateKeyFile",
|
||||
lineage.key_path, vhost.path))
|
||||
|
||||
def _enable_ocsp_stapling(self, ssl_vhost, unused_options):
|
||||
"""Enables OCSP Stapling
|
||||
|
||||
@@ -2158,3 +2294,177 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
||||
# to be modified.
|
||||
return common.install_version_controlled_file(options_ssl, options_ssl_digest,
|
||||
self.constant("MOD_SSL_CONF_SRC"), constants.ALL_SSL_OPTIONS_HASHES)
|
||||
|
||||
def enable_autohsts(self, _unused_lineage, domains):
|
||||
"""
|
||||
Enable the AutoHSTS enhancement for defined domains
|
||||
|
||||
:param _unused_lineage: Certificate lineage object, unused
|
||||
:type _unused_lineage: certbot.storage.RenewableCert
|
||||
|
||||
:param domains: List of domains in certificate to enhance
|
||||
:type domains: str
|
||||
"""
|
||||
|
||||
self._autohsts_fetch_state()
|
||||
_enhanced_vhosts = []
|
||||
for d in domains:
|
||||
matched_vhosts = self.choose_vhosts(d, create_if_no_ssl=False)
|
||||
# We should be handling only SSL vhosts for AutoHSTS
|
||||
vhosts = [vhost for vhost in matched_vhosts if vhost.ssl]
|
||||
|
||||
if not vhosts:
|
||||
msg_tmpl = ("Certbot was not able to find SSL VirtualHost for a "
|
||||
"domain {0} for enabling AutoHSTS enhancement.")
|
||||
msg = msg_tmpl.format(d)
|
||||
logger.warning(msg)
|
||||
raise errors.PluginError(msg)
|
||||
for vh in vhosts:
|
||||
try:
|
||||
self._enable_autohsts_domain(vh)
|
||||
_enhanced_vhosts.append(vh)
|
||||
except errors.PluginEnhancementAlreadyPresent:
|
||||
if vh in _enhanced_vhosts:
|
||||
continue
|
||||
msg = ("VirtualHost for domain {0} in file {1} has a " +
|
||||
"String-Transport-Security header present, exiting.")
|
||||
raise errors.PluginEnhancementAlreadyPresent(
|
||||
msg.format(d, vh.filep))
|
||||
if _enhanced_vhosts:
|
||||
note_msg = "Enabling AutoHSTS"
|
||||
self.save(note_msg)
|
||||
logger.info(note_msg)
|
||||
self.restart()
|
||||
|
||||
# Save the current state to pluginstorage
|
||||
self._autohsts_save_state()
|
||||
|
||||
def _enable_autohsts_domain(self, ssl_vhost):
|
||||
"""Do the initial AutoHSTS deployment to a vhost
|
||||
|
||||
:param ssl_vhost: The VirtualHost object to deploy the AutoHSTS
|
||||
:type ssl_vhost: :class:`~certbot_apache.obj.VirtualHost` or None
|
||||
|
||||
:raises errors.PluginEnhancementAlreadyPresent: When already enhanced
|
||||
|
||||
"""
|
||||
# This raises the exception
|
||||
self._verify_no_matching_http_header(ssl_vhost,
|
||||
"Strict-Transport-Security")
|
||||
|
||||
if "headers_module" not in self.parser.modules:
|
||||
self.enable_mod("headers")
|
||||
# Prepare the HSTS header value
|
||||
hsts_header = constants.HEADER_ARGS["Strict-Transport-Security"][:-1]
|
||||
initial_maxage = constants.AUTOHSTS_STEPS[0]
|
||||
hsts_header.append("\"max-age={0}\"".format(initial_maxage))
|
||||
|
||||
# Add ID to the VirtualHost for mapping back to it later
|
||||
uniq_id = self.add_vhost_id(ssl_vhost)
|
||||
self.save_notes += "Adding unique ID {0} to VirtualHost in {1}\n".format(
|
||||
uniq_id, ssl_vhost.filep)
|
||||
# Add the actual HSTS header
|
||||
self.parser.add_dir(ssl_vhost.path, "Header", hsts_header)
|
||||
note_msg = ("Adding gradually increasing HSTS header with initial value "
|
||||
"of {0} to VirtualHost in {1}\n".format(
|
||||
initial_maxage, ssl_vhost.filep))
|
||||
self.save_notes += note_msg
|
||||
|
||||
# Save the current state to pluginstorage
|
||||
self._autohsts[uniq_id] = {"laststep": 0, "timestamp": time.time()}
|
||||
|
||||
def update_autohsts(self, _unused_domain):
|
||||
"""
|
||||
Increase the AutoHSTS values of VirtualHosts that the user has enabled
|
||||
this enhancement for.
|
||||
|
||||
:param _unused_domain: Not currently used
|
||||
:type _unused_domain: Not Available
|
||||
|
||||
"""
|
||||
self._autohsts_fetch_state()
|
||||
if not self._autohsts:
|
||||
# No AutoHSTS enabled for any domain
|
||||
return
|
||||
curtime = time.time()
|
||||
save_and_restart = False
|
||||
for id_str, config in list(self._autohsts.items()):
|
||||
if config["timestamp"] + constants.AUTOHSTS_FREQ > curtime:
|
||||
# Skip if last increase was < AUTOHSTS_FREQ ago
|
||||
continue
|
||||
nextstep = config["laststep"] + 1
|
||||
if nextstep < len(constants.AUTOHSTS_STEPS):
|
||||
# Have not reached the max value yet
|
||||
try:
|
||||
vhost = self.find_vhost_by_id(id_str)
|
||||
except errors.PluginError:
|
||||
msg = ("Could not find VirtualHost with ID {0}, disabling "
|
||||
"AutoHSTS for this VirtualHost").format(id_str)
|
||||
logger.warning(msg)
|
||||
# Remove the orphaned AutoHSTS entry from pluginstorage
|
||||
self._autohsts.pop(id_str)
|
||||
continue
|
||||
self._autohsts_increase(vhost, id_str, nextstep)
|
||||
msg = ("Increasing HSTS max-age value for VirtualHost with id "
|
||||
"{0}").format(id_str)
|
||||
self.save_notes += msg
|
||||
save_and_restart = True
|
||||
|
||||
if save_and_restart:
|
||||
self.save("Increased HSTS max-age values")
|
||||
self.restart()
|
||||
|
||||
self._autohsts_save_state()
|
||||
|
||||
def deploy_autohsts(self, lineage):
|
||||
"""
|
||||
Checks if autohsts vhost has reached maximum auto-increased value
|
||||
and changes the HSTS max-age to a high value.
|
||||
|
||||
:param lineage: Certificate lineage object
|
||||
:type lineage: certbot.storage.RenewableCert
|
||||
"""
|
||||
self._autohsts_fetch_state()
|
||||
if not self._autohsts:
|
||||
# No autohsts enabled for any vhost
|
||||
return
|
||||
|
||||
vhosts = []
|
||||
affected_ids = []
|
||||
# Copy, as we are removing from the dict inside the loop
|
||||
for id_str, config in list(self._autohsts.items()):
|
||||
if config["laststep"]+1 >= len(constants.AUTOHSTS_STEPS):
|
||||
# max value reached, try to make permanent
|
||||
try:
|
||||
vhost = self.find_vhost_by_id(id_str)
|
||||
except errors.PluginError:
|
||||
msg = ("VirtualHost with id {} was not found, unable to "
|
||||
"make HSTS max-age permanent.").format(id_str)
|
||||
logger.warning(msg)
|
||||
self._autohsts.pop(id_str)
|
||||
continue
|
||||
if self._autohsts_vhost_in_lineage(vhost, lineage):
|
||||
vhosts.append(vhost)
|
||||
affected_ids.append(id_str)
|
||||
|
||||
save_and_restart = False
|
||||
for vhost in vhosts:
|
||||
self._autohsts_write(vhost, constants.AUTOHSTS_PERMANENT)
|
||||
msg = ("Strict-Transport-Security max-age value for "
|
||||
"VirtualHost in {0} was made permanent.").format(vhost.filep)
|
||||
logger.debug(msg)
|
||||
self.save_notes += msg+"\n"
|
||||
save_and_restart = True
|
||||
|
||||
if save_and_restart:
|
||||
self.save("Made HSTS max-age permanent")
|
||||
self.restart()
|
||||
|
||||
for id_str in affected_ids:
|
||||
self._autohsts.pop(id_str)
|
||||
|
||||
# Update AutoHSTS storage (We potentially removed vhosts from managed)
|
||||
self._autohsts_save_state()
|
||||
|
||||
|
||||
AutoHSTSEnhancement.register(ApacheConfigurator) # pylint: disable=no-member
|
||||
|
||||
@@ -48,3 +48,16 @@ UIR_ARGS = ["always", "set", "Content-Security-Policy",
|
||||
|
||||
HEADER_ARGS = {"Strict-Transport-Security": HSTS_ARGS,
|
||||
"Upgrade-Insecure-Requests": UIR_ARGS}
|
||||
|
||||
AUTOHSTS_STEPS = [60, 300, 900, 3600, 21600, 43200, 86400]
|
||||
"""AutoHSTS increase steps: 1min, 5min, 15min, 1h, 6h, 12h, 24h"""
|
||||
|
||||
AUTOHSTS_PERMANENT = 31536000
|
||||
"""Value for the last max-age of HSTS"""
|
||||
|
||||
AUTOHSTS_FREQ = 172800
|
||||
"""Minimum time since last increase to perform a new one: 48h"""
|
||||
|
||||
MANAGED_COMMENT = "DO NOT REMOVE - Managed by Certbot"
|
||||
MANAGED_COMMENT_ID = MANAGED_COMMENT+", VirtualHost id: {0}"
|
||||
"""Managed by Certbot comments and the VirtualHost identification template"""
|
||||
|
||||
@@ -16,6 +16,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ApacheParser(object):
|
||||
# pylint: disable=too-many-public-methods
|
||||
"""Class handles the fine details of parsing the Apache Configuration.
|
||||
|
||||
.. todo:: Make parsing general... remove sites-available etc...
|
||||
@@ -350,6 +351,37 @@ class ApacheParser(object):
|
||||
else:
|
||||
self.aug.set(first_dir + "/arg", args)
|
||||
|
||||
def add_comment(self, aug_conf_path, comment):
|
||||
"""Adds the comment to the augeas path
|
||||
|
||||
:param str aug_conf_path: Augeas configuration path to add directive
|
||||
:param str comment: Comment content
|
||||
|
||||
"""
|
||||
self.aug.set(aug_conf_path + "/#comment[last() + 1]", comment)
|
||||
|
||||
def find_comments(self, arg, start=None):
|
||||
"""Finds a comment with specified content from the provided DOM path
|
||||
|
||||
:param str arg: Comment content to search
|
||||
:param str start: Beginning Augeas path to begin looking
|
||||
|
||||
:returns: List of augeas paths containing the comment content
|
||||
:rtype: list
|
||||
|
||||
"""
|
||||
if not start:
|
||||
start = get_aug_path(self.root)
|
||||
|
||||
comments = self.aug.match("%s//*[label() = '#comment']" % start)
|
||||
|
||||
results = []
|
||||
for comment in comments:
|
||||
c_content = self.aug.get(comment)
|
||||
if c_content and arg in c_content:
|
||||
results.append(comment)
|
||||
return results
|
||||
|
||||
def find_dir(self, directive, arg=None, start=None, exclude=True):
|
||||
"""Finds directive in the configuration.
|
||||
|
||||
|
||||
@@ -46,6 +46,7 @@ function Cleanup() {
|
||||
|
||||
# if our environment asks us to enable modules, do our best!
|
||||
if [ "$1" = --debian-modules ] ; then
|
||||
sudo apt-get install -y apache2
|
||||
sudo apt-get install -y libapache2-mod-wsgi
|
||||
sudo apt-get install -y libapache2-mod-macro
|
||||
|
||||
|
||||
181
certbot-apache/certbot_apache/tests/autohsts_test.py
Normal file
181
certbot-apache/certbot_apache/tests/autohsts_test.py
Normal file
@@ -0,0 +1,181 @@
|
||||
# pylint: disable=too-many-public-methods,too-many-lines
|
||||
"""Test for certbot_apache.configurator AutoHSTS functionality"""
|
||||
import re
|
||||
import unittest
|
||||
import mock
|
||||
# six is used in mock.patch()
|
||||
import six # pylint: disable=unused-import
|
||||
|
||||
from certbot import errors
|
||||
from certbot_apache import constants
|
||||
from certbot_apache.tests import util
|
||||
|
||||
|
||||
class AutoHSTSTest(util.ApacheTest):
|
||||
"""Tests for AutoHSTS feature"""
|
||||
# pylint: disable=protected-access
|
||||
|
||||
def setUp(self): # pylint: disable=arguments-differ
|
||||
super(AutoHSTSTest, self).setUp()
|
||||
|
||||
self.config = util.get_apache_configurator(
|
||||
self.config_path, self.vhost_path, self.config_dir, self.work_dir)
|
||||
self.config.parser.modules.add("headers_module")
|
||||
self.config.parser.modules.add("mod_headers.c")
|
||||
self.config.parser.modules.add("ssl_module")
|
||||
self.config.parser.modules.add("mod_ssl.c")
|
||||
|
||||
self.vh_truth = util.get_vh_truth(
|
||||
self.temp_dir, "debian_apache_2_4/multiple_vhosts")
|
||||
|
||||
def get_autohsts_value(self, vh_path):
|
||||
""" Get value from Strict-Transport-Security header """
|
||||
header_path = self.config.parser.find_dir("Header", None, vh_path)
|
||||
if header_path:
|
||||
pat = '(?:[ "]|^)(strict-transport-security)(?:[ "]|$)'
|
||||
for head in header_path:
|
||||
if re.search(pat, self.config.parser.aug.get(head).lower()):
|
||||
return self.config.parser.aug.get(head.replace("arg[3]",
|
||||
"arg[4]"))
|
||||
|
||||
@mock.patch("certbot_apache.configurator.ApacheConfigurator.restart")
|
||||
@mock.patch("certbot_apache.configurator.ApacheConfigurator.enable_mod")
|
||||
def test_autohsts_enable_headers_mod(self, mock_enable, _restart):
|
||||
self.config.parser.modules.discard("headers_module")
|
||||
self.config.parser.modules.discard("mod_header.c")
|
||||
self.config.enable_autohsts(mock.MagicMock(), ["ocspvhost.com"])
|
||||
self.assertTrue(mock_enable.called)
|
||||
|
||||
@mock.patch("certbot_apache.configurator.ApacheConfigurator.restart")
|
||||
def test_autohsts_deploy_already_exists(self, _restart):
|
||||
self.config.enable_autohsts(mock.MagicMock(), ["ocspvhost.com"])
|
||||
self.assertRaises(errors.PluginEnhancementAlreadyPresent,
|
||||
self.config.enable_autohsts,
|
||||
mock.MagicMock(), ["ocspvhost.com"])
|
||||
|
||||
@mock.patch("certbot_apache.constants.AUTOHSTS_FREQ", 0)
|
||||
@mock.patch("certbot_apache.configurator.ApacheConfigurator.restart")
|
||||
def test_autohsts_increase(self, _mock_restart):
|
||||
maxage = "\"max-age={0}\""
|
||||
initial_val = maxage.format(constants.AUTOHSTS_STEPS[0])
|
||||
inc_val = maxage.format(constants.AUTOHSTS_STEPS[1])
|
||||
|
||||
self.config.enable_autohsts(mock.MagicMock(), ["ocspvhost.com"])
|
||||
# Verify initial value
|
||||
self.assertEquals(self.get_autohsts_value(self.vh_truth[7].path),
|
||||
initial_val)
|
||||
# Increase
|
||||
self.config.update_autohsts(mock.MagicMock())
|
||||
# Verify increased value
|
||||
self.assertEquals(self.get_autohsts_value(self.vh_truth[7].path),
|
||||
inc_val)
|
||||
|
||||
@mock.patch("certbot_apache.configurator.ApacheConfigurator.restart")
|
||||
@mock.patch("certbot_apache.configurator.ApacheConfigurator._autohsts_increase")
|
||||
def test_autohsts_increase_noop(self, mock_increase, _restart):
|
||||
maxage = "\"max-age={0}\""
|
||||
initial_val = maxage.format(constants.AUTOHSTS_STEPS[0])
|
||||
self.config.enable_autohsts(mock.MagicMock(), ["ocspvhost.com"])
|
||||
# Verify initial value
|
||||
self.assertEquals(self.get_autohsts_value(self.vh_truth[7].path),
|
||||
initial_val)
|
||||
|
||||
self.config.update_autohsts(mock.MagicMock())
|
||||
# Freq not patched, so value shouldn't increase
|
||||
self.assertFalse(mock_increase.called)
|
||||
|
||||
|
||||
@mock.patch("certbot_apache.configurator.ApacheConfigurator.restart")
|
||||
@mock.patch("certbot_apache.constants.AUTOHSTS_FREQ", 0)
|
||||
def test_autohsts_increase_no_header(self, _restart):
|
||||
self.config.enable_autohsts(mock.MagicMock(), ["ocspvhost.com"])
|
||||
# Remove the header
|
||||
dir_locs = self.config.parser.find_dir("Header", None,
|
||||
self.vh_truth[7].path)
|
||||
dir_loc = "/".join(dir_locs[0].split("/")[:-1])
|
||||
self.config.parser.aug.remove(dir_loc)
|
||||
self.assertRaises(errors.PluginError,
|
||||
self.config.update_autohsts,
|
||||
mock.MagicMock())
|
||||
|
||||
@mock.patch("certbot_apache.constants.AUTOHSTS_FREQ", 0)
|
||||
@mock.patch("certbot_apache.configurator.ApacheConfigurator.restart")
|
||||
def test_autohsts_increase_and_make_permanent(self, _mock_restart):
|
||||
maxage = "\"max-age={0}\""
|
||||
max_val = maxage.format(constants.AUTOHSTS_PERMANENT)
|
||||
mock_lineage = mock.MagicMock()
|
||||
mock_lineage.key_path = "/etc/apache2/ssl/key-certbot_15.pem"
|
||||
self.config.enable_autohsts(mock.MagicMock(), ["ocspvhost.com"])
|
||||
for i in range(len(constants.AUTOHSTS_STEPS)-1):
|
||||
# Ensure that value is not made permanent prematurely
|
||||
self.config.deploy_autohsts(mock_lineage)
|
||||
self.assertNotEquals(self.get_autohsts_value(self.vh_truth[7].path),
|
||||
max_val)
|
||||
self.config.update_autohsts(mock.MagicMock())
|
||||
# Value should match pre-permanent increment step
|
||||
cur_val = maxage.format(constants.AUTOHSTS_STEPS[i+1])
|
||||
self.assertEquals(self.get_autohsts_value(self.vh_truth[7].path),
|
||||
cur_val)
|
||||
# Make permanent
|
||||
self.config.deploy_autohsts(mock_lineage)
|
||||
self.assertEquals(self.get_autohsts_value(self.vh_truth[7].path),
|
||||
max_val)
|
||||
|
||||
def test_autohsts_update_noop(self):
|
||||
with mock.patch("time.time") as mock_time:
|
||||
# Time mock is used to make sure that the execution does not
|
||||
# continue when no autohsts entries exist in pluginstorage
|
||||
self.config.update_autohsts(mock.MagicMock())
|
||||
self.assertFalse(mock_time.called)
|
||||
|
||||
def test_autohsts_make_permanent_noop(self):
|
||||
self.config.storage.put = mock.MagicMock()
|
||||
self.config.deploy_autohsts(mock.MagicMock())
|
||||
# Make sure that the execution does not continue when no entries in store
|
||||
self.assertFalse(self.config.storage.put.called)
|
||||
|
||||
@mock.patch("certbot_apache.display_ops.select_vhost")
|
||||
def test_autohsts_no_ssl_vhost(self, mock_select):
|
||||
mock_select.return_value = self.vh_truth[0]
|
||||
with mock.patch("certbot_apache.configurator.logger.warning") as mock_log:
|
||||
self.assertRaises(errors.PluginError,
|
||||
self.config.enable_autohsts,
|
||||
mock.MagicMock(), "invalid.example.com")
|
||||
self.assertTrue(
|
||||
"Certbot was not able to find SSL" in mock_log.call_args[0][0])
|
||||
|
||||
@mock.patch("certbot_apache.configurator.ApacheConfigurator.restart")
|
||||
@mock.patch("certbot_apache.configurator.ApacheConfigurator.add_vhost_id")
|
||||
def test_autohsts_dont_enhance_twice(self, mock_id, _restart):
|
||||
mock_id.return_value = "1234567"
|
||||
self.config.enable_autohsts(mock.MagicMock(),
|
||||
["ocspvhost.com", "ocspvhost.com"])
|
||||
self.assertEquals(mock_id.call_count, 1)
|
||||
|
||||
def test_autohsts_remove_orphaned(self):
|
||||
# pylint: disable=protected-access
|
||||
self.config._autohsts_fetch_state()
|
||||
self.config._autohsts["orphan_id"] = {"laststep": 0, "timestamp": 0}
|
||||
|
||||
self.config._autohsts_save_state()
|
||||
self.config.update_autohsts(mock.MagicMock())
|
||||
self.assertFalse("orphan_id" in self.config._autohsts)
|
||||
# Make sure it's removed from the pluginstorage file as well
|
||||
self.config._autohsts = None
|
||||
self.config._autohsts_fetch_state()
|
||||
self.assertFalse(self.config._autohsts)
|
||||
|
||||
def test_autohsts_make_permanent_vhost_not_found(self):
|
||||
# pylint: disable=protected-access
|
||||
self.config._autohsts_fetch_state()
|
||||
self.config._autohsts["orphan_id"] = {"laststep": 999, "timestamp": 0}
|
||||
self.config._autohsts_save_state()
|
||||
with mock.patch("certbot_apache.configurator.logger.warning") as mock_log:
|
||||
self.config.deploy_autohsts(mock.MagicMock())
|
||||
self.assertTrue(mock_log.called)
|
||||
self.assertTrue(
|
||||
"VirtualHost with id orphan_id was not" in mock_log.call_args[0][0])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main() # pragma: no cover
|
||||
@@ -1487,6 +1487,21 @@ class MultipleVhostsTest(util.ApacheTest):
|
||||
"Upgrade-Insecure-Requests")
|
||||
self.assertTrue(mock_choose.called)
|
||||
|
||||
def test_add_vhost_id(self):
|
||||
for vh in [self.vh_truth[0], self.vh_truth[1], self.vh_truth[2]]:
|
||||
vh_id = self.config.add_vhost_id(vh)
|
||||
self.assertEqual(vh, self.config.find_vhost_by_id(vh_id))
|
||||
|
||||
def test_find_vhost_by_id_404(self):
|
||||
self.assertRaises(errors.PluginError,
|
||||
self.config.find_vhost_by_id,
|
||||
"nonexistent")
|
||||
|
||||
def test_add_vhost_id_already_exists(self):
|
||||
first_id = self.config.add_vhost_id(self.vh_truth[0])
|
||||
second_id = self.config.add_vhost_id(self.vh_truth[0])
|
||||
self.assertEqual(first_id, second_id)
|
||||
|
||||
|
||||
class AugeasVhostsTest(util.ApacheTest):
|
||||
"""Test vhosts with illegal names dependent on augeas version."""
|
||||
|
||||
@@ -299,6 +299,13 @@ class BasicParserTest(util.ParserTest):
|
||||
errors.MisconfigurationError,
|
||||
self.parser.update_runtime_variables)
|
||||
|
||||
def test_add_comment(self):
|
||||
from certbot_apache.parser import get_aug_path
|
||||
self.parser.add_comment(get_aug_path(self.parser.loc["name"]), "123456")
|
||||
comm = self.parser.find_comments("123456")
|
||||
self.assertEquals(len(comm), 1)
|
||||
self.assertTrue(self.parser.loc["name"] in comm[0])
|
||||
|
||||
|
||||
class ParserInitTest(util.ApacheTest):
|
||||
def setUp(self): # pylint: disable=arguments-differ
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
-e acme[dev]
|
||||
certbot[dev]==0.21.1
|
||||
acme[dev]==0.25.0
|
||||
-e .[dev]
|
||||
|
||||
@@ -2,13 +2,13 @@ from setuptools import setup
|
||||
from setuptools import find_packages
|
||||
|
||||
|
||||
version = '0.25.0.dev0'
|
||||
version = '0.26.0.dev0'
|
||||
|
||||
# Remember to update local-oldest-requirements.txt when changing the minimum
|
||||
# acme/certbot version.
|
||||
install_requires = [
|
||||
'acme>0.24.0',
|
||||
'certbot>=0.21.1',
|
||||
'acme>=0.25.0',
|
||||
'certbot>=0.26.0.dev0',
|
||||
'mock',
|
||||
'python-augeas',
|
||||
'setuptools',
|
||||
|
||||
47
certbot-auto
47
certbot-auto
@@ -31,7 +31,7 @@ if [ -z "$VENV_PATH" ]; then
|
||||
fi
|
||||
VENV_BIN="$VENV_PATH/bin"
|
||||
BOOTSTRAP_VERSION_PATH="$VENV_PATH/certbot-auto-bootstrap-version.txt"
|
||||
LE_AUTO_VERSION="0.24.0"
|
||||
LE_AUTO_VERSION="0.25.1"
|
||||
BASENAME=$(basename $0)
|
||||
USAGE="Usage: $BASENAME [OPTIONS]
|
||||
A self-updating wrapper script for the Certbot ACME client. When run, updates
|
||||
@@ -1055,9 +1055,11 @@ cffi==1.10.0 \
|
||||
--hash=sha256:5576644b859197da7bbd8f8c7c2fb5dcc6cd505cadb42992d5f104c013f8a214 \
|
||||
--hash=sha256:b3b02911eb1f6ada203b0763ba924234629b51586f72a21faacc638269f4ced5
|
||||
ConfigArgParse==0.12.0 \
|
||||
--hash=sha256:28cd7d67669651f2a4518367838c49539457504584a139709b2b8f6c208ef339
|
||||
--hash=sha256:28cd7d67669651f2a4518367838c49539457504584a139709b2b8f6c208ef339 \
|
||||
--no-binary ConfigArgParse
|
||||
configobj==5.0.6 \
|
||||
--hash=sha256:a2f5650770e1c87fb335af19a9b7eb73fc05ccf22144eb68db7d00cd2bcb0902
|
||||
--hash=sha256:a2f5650770e1c87fb335af19a9b7eb73fc05ccf22144eb68db7d00cd2bcb0902 \
|
||||
--no-binary configobj
|
||||
cryptography==2.0.2 \
|
||||
--hash=sha256:187ae17358436d2c760f28c2aeb02fefa3f37647a9c5b6f7f7c3e83cd1c5a972 \
|
||||
--hash=sha256:19e43a13bbf52028dd1e810c803f2ad8880d0692d772f98d42e1eaf34bdee3d6 \
|
||||
@@ -1112,7 +1114,8 @@ mock==1.3.0 \
|
||||
--hash=sha256:3f573a18be94de886d1191f27c168427ef693e8dcfcecf95b170577b2eb69cbb \
|
||||
--hash=sha256:1e247dbecc6ce057299eb7ee019ad68314bb93152e81d9a6110d35f4d5eca0f6
|
||||
ordereddict==1.1 \
|
||||
--hash=sha256:1c35b4ac206cef2d24816c89f89cf289dd3d38cf7c449bb3fab7bf6d43f01b1f
|
||||
--hash=sha256:1c35b4ac206cef2d24816c89f89cf289dd3d38cf7c449bb3fab7bf6d43f01b1f \
|
||||
--no-binary ordereddict
|
||||
packaging==16.8 \
|
||||
--hash=sha256:99276dc6e3a7851f32027a68f1095cd3f77c148091b092ea867a351811cfe388 \
|
||||
--hash=sha256:5d50835fdf0a7edf0b55e311b7c887786504efea1177abd7e69329a8e5ea619e
|
||||
@@ -1138,7 +1141,8 @@ pyRFC3339==1.0 \
|
||||
--hash=sha256:eea31835c56e2096af4363a5745a784878a61d043e247d3a6d6a0a32a9741f56 \
|
||||
--hash=sha256:8dfbc6c458b8daba1c0f3620a8c78008b323a268b27b7359e92a4ae41325f535
|
||||
python-augeas==0.5.0 \
|
||||
--hash=sha256:67d59d66cdba8d624e0389b87b2a83a176f21f16a87553b50f5703b23f29bac2
|
||||
--hash=sha256:67d59d66cdba8d624e0389b87b2a83a176f21f16a87553b50f5703b23f29bac2 \
|
||||
--no-binary python-augeas
|
||||
pytz==2015.7 \
|
||||
--hash=sha256:3abe6a6d3fc2fbbe4c60144211f45da2edbe3182a6f6511af6bbba0598b1f992 \
|
||||
--hash=sha256:939ef9c1e1224d980405689a97ffcf7828c56d1517b31d73464356c1f2b7769e \
|
||||
@@ -1166,9 +1170,11 @@ unittest2==1.1.0 \
|
||||
--hash=sha256:13f77d0875db6d9b435e1d4f41e74ad4cc2eb6e1d5c824996092b3430f088bb8 \
|
||||
--hash=sha256:22882a0e418c284e1f718a822b3b022944d53d2d908e1690b319a9d3eb2c0579
|
||||
zope.component==4.2.2 \
|
||||
--hash=sha256:282c112b55dd8e3c869a3571f86767c150ab1284a9ace2bdec226c592acaf81a
|
||||
--hash=sha256:282c112b55dd8e3c869a3571f86767c150ab1284a9ace2bdec226c592acaf81a \
|
||||
--no-binary zope.component
|
||||
zope.event==4.1.0 \
|
||||
--hash=sha256:dc7a59a2fd91730d3793131a5d261b29e93ec4e2a97f1bc487ce8defee2fe786
|
||||
--hash=sha256:dc7a59a2fd91730d3793131a5d261b29e93ec4e2a97f1bc487ce8defee2fe786 \
|
||||
--no-binary zope.event
|
||||
zope.interface==4.1.3 \
|
||||
--hash=sha256:f07b631f7a601cd8cbd3332d54f43142c7088a83299f859356f08d1d4d4259b3 \
|
||||
--hash=sha256:de5cca083b9439d8002fb76bbe6b4998c5a5a721fab25b84298967f002df4c94 \
|
||||
@@ -1187,6 +1193,9 @@ zope.interface==4.1.3 \
|
||||
--hash=sha256:928138365245a0e8869a5999fbcc2a45475a0a6ed52a494d60dbdc540335fedd \
|
||||
--hash=sha256:0d841ba1bb840eea0e6489dc5ecafa6125554971f53b5acb87764441e61bceba \
|
||||
--hash=sha256:b09c8c1d47b3531c400e0195697f1414a63221de6ef478598a4f1460f7d9a392
|
||||
requests-toolbelt==0.8.0 \
|
||||
--hash=sha256:42c9c170abc2cacb78b8ab23ac957945c7716249206f90874651971a4acff237 \
|
||||
--hash=sha256:f6a531936c6fa4c6cfce1b9c10d5c4f498d16528d2a54a22ca00011205a187b5
|
||||
|
||||
# Contains the requirements for the letsencrypt package.
|
||||
#
|
||||
@@ -1199,18 +1208,18 @@ letsencrypt==0.7.0 \
|
||||
--hash=sha256:105a5fb107e45bcd0722eb89696986dcf5f08a86a321d6aef25a0c7c63375ade \
|
||||
--hash=sha256:c36e532c486a7e92155ee09da54b436a3c420813ec1c590b98f635d924720de9
|
||||
|
||||
certbot==0.24.0 \
|
||||
--hash=sha256:a3fc41fde4f0dbb35f7ebec2f9e00339580b3f4298850411eac0719223073b27 \
|
||||
--hash=sha256:a072d4528bb3ac4184f5c961a96931795ddfe4b7cb0f3a98954bdd4cce5f6d70
|
||||
acme==0.24.0 \
|
||||
--hash=sha256:b92b16102051f447abb52917638fbfb34b646ac07267fee85961b360a0149e32 \
|
||||
--hash=sha256:d655e0627c0830114ab3f6732d8bf2f4a2c36f602e0cde10988684e229b501cb
|
||||
certbot-apache==0.24.0 \
|
||||
--hash=sha256:fe54db3e7e09ffe1139041c23ff5123e80aa1526d6fcd40b2a593d005cfcf152 \
|
||||
--hash=sha256:686c6c0af5ae8d06e37cc762de7ffa0dc5c3b1ba06ff7653ef61713fa016f891
|
||||
certbot-nginx==0.24.0 \
|
||||
--hash=sha256:d44c419f72c2cc30de3b138a2cf92e0531696dcb048f287036e229dce2131c00 \
|
||||
--hash=sha256:3283d1db057261f05537fa408baee20e0ab9e81c3d55cfba70afe3805cd6f941
|
||||
certbot==0.25.1 \
|
||||
--hash=sha256:01689015364685fef3f1e1fb7832ba84eb3b0aa85bc5a71c96661f6d4c59981f \
|
||||
--hash=sha256:5c23e5186133bb1afd805be5e0cd2fb7b95862a8b0459c9ecad4ae60f933e54e
|
||||
acme==0.25.1 \
|
||||
--hash=sha256:26e641a01536705fe5f12d856703b8ef06e5a07981a7b6379d2771dcdb69a742 \
|
||||
--hash=sha256:47b5f3f73d69b7b1d13f918aa2cd75a8093069a68becf4af38e428e4613b2734
|
||||
certbot-apache==0.25.1 \
|
||||
--hash=sha256:a28b7c152cc11474bef5b5e7967aaea42b2c0aaf86fd82ee4082713d33cee5a9 \
|
||||
--hash=sha256:ed012465617073a0f1057fe854dc8d1eb6d2dd7ede1fb2eee765129fed2a095a
|
||||
certbot-nginx==0.25.1 \
|
||||
--hash=sha256:83f82c3ba08c0b1d4bf449ac24018e8e7dd34a6248d35466f2de7da1cd312e15 \
|
||||
--hash=sha256:68f98b41c54e0bf4218ef293079597176617bee3837ae3aa6528ce2ff0bf4f9c
|
||||
|
||||
UNLIKELY_EOF
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
@@ -4,7 +4,7 @@ from setuptools import setup
|
||||
from setuptools import find_packages
|
||||
|
||||
|
||||
version = '0.25.0.dev0'
|
||||
version = '0.26.0.dev0'
|
||||
|
||||
install_requires = [
|
||||
'certbot',
|
||||
|
||||
@@ -2,7 +2,7 @@ from setuptools import setup
|
||||
from setuptools import find_packages
|
||||
|
||||
|
||||
version = '0.25.0.dev0'
|
||||
version = '0.26.0.dev0'
|
||||
|
||||
# Remember to update local-oldest-requirements.txt when changing the minimum
|
||||
# acme/certbot version.
|
||||
|
||||
@@ -2,7 +2,7 @@ from setuptools import setup
|
||||
from setuptools import find_packages
|
||||
|
||||
|
||||
version = '0.25.0.dev0'
|
||||
version = '0.26.0.dev0'
|
||||
|
||||
# Remember to update local-oldest-requirements.txt when changing the minimum
|
||||
# acme/certbot version.
|
||||
|
||||
@@ -2,7 +2,7 @@ from setuptools import setup
|
||||
from setuptools import find_packages
|
||||
|
||||
|
||||
version = '0.25.0.dev0'
|
||||
version = '0.26.0.dev0'
|
||||
|
||||
# Remember to update local-oldest-requirements.txt when changing the minimum
|
||||
# acme/certbot version.
|
||||
|
||||
@@ -2,7 +2,7 @@ from setuptools import setup
|
||||
from setuptools import find_packages
|
||||
|
||||
|
||||
version = '0.25.0.dev0'
|
||||
version = '0.26.0.dev0'
|
||||
|
||||
# Remember to update local-oldest-requirements.txt when changing the minimum
|
||||
# acme/certbot version.
|
||||
|
||||
@@ -2,7 +2,7 @@ from setuptools import setup
|
||||
from setuptools import find_packages
|
||||
|
||||
|
||||
version = '0.25.0.dev0'
|
||||
version = '0.26.0.dev0'
|
||||
|
||||
# Remember to update local-oldest-requirements.txt when changing the minimum
|
||||
# acme/certbot version.
|
||||
|
||||
@@ -2,7 +2,7 @@ from setuptools import setup
|
||||
from setuptools import find_packages
|
||||
|
||||
|
||||
version = '0.25.0.dev0'
|
||||
version = '0.26.0.dev0'
|
||||
|
||||
# Remember to update local-oldest-requirements.txt when changing the minimum
|
||||
# acme/certbot version.
|
||||
|
||||
@@ -2,7 +2,7 @@ from setuptools import setup
|
||||
from setuptools import find_packages
|
||||
|
||||
|
||||
version = '0.25.0.dev0'
|
||||
version = '0.26.0.dev0'
|
||||
|
||||
# Remember to update local-oldest-requirements.txt when changing the minimum
|
||||
# acme/certbot version.
|
||||
|
||||
@@ -2,7 +2,7 @@ from setuptools import setup
|
||||
from setuptools import find_packages
|
||||
|
||||
|
||||
version = '0.25.0.dev0'
|
||||
version = '0.26.0.dev0'
|
||||
|
||||
# Remember to update local-oldest-requirements.txt when changing the minimum
|
||||
# acme/certbot version.
|
||||
|
||||
@@ -2,7 +2,7 @@ from setuptools import setup
|
||||
from setuptools import find_packages
|
||||
|
||||
|
||||
version = '0.25.0.dev0'
|
||||
version = '0.26.0.dev0'
|
||||
|
||||
# Remember to update local-oldest-requirements.txt when changing the minimum
|
||||
# acme/certbot version.
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
-e acme[dev]
|
||||
acme[dev]==0.25.0
|
||||
certbot[dev]==0.21.1
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
from setuptools import setup
|
||||
from setuptools import find_packages
|
||||
|
||||
version = '0.25.0.dev0'
|
||||
version = '0.26.0.dev0'
|
||||
|
||||
# Remember to update local-oldest-requirements.txt when changing the minimum
|
||||
# acme/certbot version.
|
||||
install_requires = [
|
||||
'acme>0.24.0',
|
||||
'acme>=0.25.0',
|
||||
'certbot>=0.21.1',
|
||||
'boto3',
|
||||
'mock',
|
||||
|
||||
@@ -69,8 +69,9 @@ class NginxConfigurator(common.Installer):
|
||||
|
||||
@classmethod
|
||||
def add_parser_arguments(cls, add):
|
||||
default_server_root = _determine_default_server_root()
|
||||
add("server-root", default=constants.CLI_DEFAULTS["server_root"],
|
||||
help="Nginx server root directory.")
|
||||
help="Nginx server root directory. (default: %s)" % default_server_root)
|
||||
add("ctl", default=constants.CLI_DEFAULTS["ctl"], help="Path to the "
|
||||
"'nginx' binary, used for 'configtest' and retrieving nginx "
|
||||
"version number.")
|
||||
@@ -1129,3 +1130,11 @@ def install_ssl_options_conf(options_ssl, options_ssl_digest):
|
||||
"""Copy Certbot's SSL options file into the system's config dir if required."""
|
||||
return common.install_version_controlled_file(options_ssl, options_ssl_digest,
|
||||
constants.MOD_SSL_CONF_SRC, constants.ALL_SSL_OPTIONS_HASHES)
|
||||
|
||||
def _determine_default_server_root():
|
||||
if os.environ.get("CERTBOT_DOCS") == "1":
|
||||
default_server_root = "%s or %s" % (constants.LINUX_SERVER_ROOT,
|
||||
constants.FREEBSD_DARWIN_SERVER_ROOT)
|
||||
else:
|
||||
default_server_root = constants.CLI_DEFAULTS["server_root"]
|
||||
return default_server_root
|
||||
|
||||
@@ -2,10 +2,13 @@
|
||||
import pkg_resources
|
||||
import platform
|
||||
|
||||
FREEBSD_DARWIN_SERVER_ROOT = "/usr/local/etc/nginx"
|
||||
LINUX_SERVER_ROOT = "/etc/nginx"
|
||||
|
||||
if platform.system() in ('FreeBSD', 'Darwin'):
|
||||
server_root_tmp = "/usr/local/etc/nginx"
|
||||
server_root_tmp = FREEBSD_DARWIN_SERVER_ROOT
|
||||
else:
|
||||
server_root_tmp = "/etc/nginx"
|
||||
server_root_tmp = LINUX_SERVER_ROOT
|
||||
|
||||
CLI_DEFAULTS = dict(
|
||||
server_root=server_root_tmp,
|
||||
|
||||
@@ -949,5 +949,30 @@ class InstallSslOptionsConfTest(util.NginxTest):
|
||||
" with the sha256 hash of self.config.mod_ssl_conf when it is updated.")
|
||||
|
||||
|
||||
class DetermineDefaultServerRootTest(certbot_test_util.ConfigTestCase):
|
||||
"""Tests for certbot_nginx.configurator._determine_default_server_root."""
|
||||
|
||||
def _call(self):
|
||||
from certbot_nginx.configurator import _determine_default_server_root
|
||||
return _determine_default_server_root()
|
||||
|
||||
@mock.patch.dict(os.environ, {"CERTBOT_DOCS": "1"})
|
||||
def test_docs_value(self):
|
||||
self._test(expect_both_values=True)
|
||||
|
||||
@mock.patch.dict(os.environ, {})
|
||||
def test_real_values(self):
|
||||
self._test(expect_both_values=False)
|
||||
|
||||
def _test(self, expect_both_values):
|
||||
server_root = self._call()
|
||||
|
||||
if expect_both_values:
|
||||
self.assertIn("/usr/local/etc/nginx", server_root)
|
||||
self.assertIn("/etc/nginx", server_root)
|
||||
else:
|
||||
self.assertTrue(server_root == "/etc/nginx" or server_root == "/usr/local/etc/nginx")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main() # pragma: no cover
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
-e acme[dev]
|
||||
acme[dev]==0.25.0
|
||||
-e .[dev]
|
||||
|
||||
@@ -2,16 +2,13 @@ from setuptools import setup
|
||||
from setuptools import find_packages
|
||||
|
||||
|
||||
version = '0.25.0.dev0'
|
||||
version = '0.26.0.dev0'
|
||||
|
||||
# Remember to update local-oldest-requirements.txt when changing the minimum
|
||||
# acme/certbot version.
|
||||
install_requires = [
|
||||
# This plugin works with an older version of acme, but Certbot does not.
|
||||
# 0.22.0 is specified here to work around
|
||||
# https://github.com/pypa/pip/issues/988.
|
||||
'acme>0.21.1',
|
||||
'certbot>0.21.1',
|
||||
'acme>=0.25.0',
|
||||
'certbot>=0.22.0',
|
||||
'mock',
|
||||
'PyOpenSSL',
|
||||
'pyparsing>=1.5.5', # Python3 support; perhaps unnecessary?
|
||||
|
||||
@@ -62,3 +62,5 @@ test_deployment_and_rollback nginx6.wtf
|
||||
# note: not reached if anything above fails, hence "killall" at the
|
||||
# top
|
||||
nginx -c $nginx_root/nginx.conf -s stop
|
||||
|
||||
coverage report --fail-under 75 --include 'certbot-nginx/*' --show-missing
|
||||
|
||||
@@ -2,9 +2,22 @@
|
||||
Postfix plugin for Certbot
|
||||
==========================
|
||||
|
||||
To install your certs with this plugin, run:
|
||||
Note: this MTA installer is in **developer beta**-- we appreciate any testing, feedback, or
|
||||
feature requests for this plugin.
|
||||
|
||||
``certbot install --installer postfix --cert-path <path to cert> --key-path <path to key> -d <MX hostname>``
|
||||
To install this plugin, in the root of this repo, run::
|
||||
|
||||
And there you go! If you'd like to obtain these certificates via certbot, there's more documentation on how to do this `here <https://certbot.eff.org/docs/using.html#getting-certificates-and-choosing-plugins>`_.
|
||||
./tools/venv.sh
|
||||
source venv/bin/activate
|
||||
|
||||
You can use this installer with any `authenticator plugin
|
||||
<https://certbot.eff.org/docs/using.html#getting-certificates-and-choosing-plugins>`_.
|
||||
For instance, with the `standalone authenticator
|
||||
<https://certbot.eff.org/docs/using.html#standalone>`_, which requires no extra server
|
||||
software, you might run::
|
||||
|
||||
sudo ./venv/bin/certbot run --standalone -i postfix -d <domain name>
|
||||
|
||||
To just install existing certs with this plugin, run::
|
||||
|
||||
sudo ./venv/bin/certbot install -i postfix --cert-path <path to cert> --key-path <path to key> -d <domain name>
|
||||
|
||||
@@ -6,7 +6,7 @@ version = '0.26.0.dev0'
|
||||
|
||||
install_requires = [
|
||||
'acme>=0.25.0',
|
||||
'certbot>0.23.0',
|
||||
'certbot>=0.23.0',
|
||||
'setuptools',
|
||||
'six',
|
||||
'zope.component',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Certbot client."""
|
||||
|
||||
# version number like 1.2.3a0, must have at least 2 parts, like 1.2
|
||||
__version__ = '0.25.0.dev0'
|
||||
__version__ = '0.26.0.dev0'
|
||||
|
||||
@@ -175,31 +175,41 @@ class AccountFileStorage(interfaces.AccountStorage):
|
||||
except errors.AccountStorageError:
|
||||
logger.debug("Account loading problem", exc_info=True)
|
||||
|
||||
|
||||
if not accounts and server_path in constants.LE_REUSE_SERVERS:
|
||||
# find all for the next link down
|
||||
prev_server_path = constants.LE_REUSE_SERVERS[server_path]
|
||||
prev_accounts = self._find_all_for_server_path(prev_server_path)
|
||||
# if we found something, link to that
|
||||
if prev_accounts:
|
||||
if os.path.islink(accounts_dir):
|
||||
os.unlink(accounts_dir)
|
||||
else:
|
||||
try:
|
||||
os.rmdir(accounts_dir)
|
||||
self._symlink_to_accounts_dir(prev_server_path, server_path)
|
||||
except OSError:
|
||||
return []
|
||||
prev_account_dir = self.config.accounts_dir_for_server_path(prev_server_path)
|
||||
os.symlink(prev_account_dir, accounts_dir)
|
||||
accounts = prev_accounts
|
||||
return accounts
|
||||
|
||||
def find_all(self):
|
||||
return self._find_all_for_server_path(self.config.server_path)
|
||||
|
||||
def _symlink_to_accounts_dir(self, prev_server_path, server_path):
|
||||
accounts_dir = self.config.accounts_dir_for_server_path(server_path)
|
||||
if os.path.islink(accounts_dir):
|
||||
os.unlink(accounts_dir)
|
||||
else:
|
||||
os.rmdir(accounts_dir)
|
||||
prev_account_dir = self.config.accounts_dir_for_server_path(prev_server_path)
|
||||
os.symlink(prev_account_dir, accounts_dir)
|
||||
|
||||
def _load_for_server_path(self, account_id, server_path):
|
||||
account_dir_path = self._account_dir_path_for_server_path(account_id, server_path)
|
||||
if not os.path.isdir(account_dir_path):
|
||||
if not os.path.isdir(account_dir_path): # isdir is also true for symlinks
|
||||
if server_path in constants.LE_REUSE_SERVERS:
|
||||
prev_server_path = constants.LE_REUSE_SERVERS[server_path]
|
||||
prev_loaded_account = self._load_for_server_path(account_id, prev_server_path)
|
||||
# we didn't error so we found something, so create a symlink to that
|
||||
self._symlink_to_accounts_dir(prev_server_path, server_path)
|
||||
return prev_loaded_account
|
||||
else:
|
||||
raise errors.AccountNotFound(
|
||||
"Account at %s does not exist" % account_dir_path)
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ from certbot import util
|
||||
|
||||
from certbot.display import util as display_util
|
||||
from certbot.plugins import disco as plugins_disco
|
||||
import certbot.plugins.enhancements as enhancements
|
||||
import certbot.plugins.selection as plugin_selection
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -627,6 +628,10 @@ class HelpfulArgumentParser(object):
|
||||
raise errors.Error("Using --allow-subset-of-names with a"
|
||||
" wildcard domain is not supported.")
|
||||
|
||||
if parsed_args.hsts and parsed_args.auto_hsts:
|
||||
raise errors.Error(
|
||||
"Parameters --hsts and --auto-hsts cannot be used simultaneously.")
|
||||
|
||||
possible_deprecation_warning(parsed_args)
|
||||
|
||||
return parsed_args
|
||||
@@ -1077,7 +1082,7 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis
|
||||
help="Show tracebacks in case of errors, and allow certbot-auto "
|
||||
"execution on experimental platforms")
|
||||
helpful.add(
|
||||
[None, "certonly", "renew", "run"], "--debug-challenges", action="store_true",
|
||||
[None, "certonly", "run"], "--debug-challenges", action="store_true",
|
||||
default=flag_default("debug_challenges"),
|
||||
help="After setting up challenges, wait for user input before "
|
||||
"submitting to CA")
|
||||
@@ -1220,10 +1225,17 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis
|
||||
" when the user executes \"certbot renew\", regardless of if the certificate"
|
||||
" is renewed. This setting does not apply to important TLS configuration"
|
||||
" updates.")
|
||||
helpful.add(
|
||||
"renew", "--no-autorenew", action="store_false",
|
||||
default=flag_default("autorenew"), dest="autorenew",
|
||||
help="Disable auto renewal of certificates.")
|
||||
|
||||
helpful.add_deprecated_argument("--agree-dev-preview", 0)
|
||||
helpful.add_deprecated_argument("--dialog", 0)
|
||||
|
||||
# Populate the command line parameters for new style enhancements
|
||||
enhancements.populate_cli(helpful.add)
|
||||
|
||||
_create_subparsers(helpful)
|
||||
_paths_parser(helpful)
|
||||
# _plugins_parsing should be the last thing to act upon the main
|
||||
|
||||
@@ -65,9 +65,17 @@ def determine_user_agent(config):
|
||||
if config.user_agent is None:
|
||||
ua = ("CertbotACMEClient/{0} ({1}; {2}{8}) Authenticator/{3} Installer/{4} "
|
||||
"({5}; flags: {6}) Py/{7}")
|
||||
ua = ua.format(certbot.__version__, cli.cli_command, util.get_os_info_ua(),
|
||||
if os.environ.get("CERTBOT_DOCS") == "1":
|
||||
cli_command = "certbot(-auto)"
|
||||
os_info = "OS_NAME OS_VERSION"
|
||||
python_version = "major.minor.patchlevel"
|
||||
else:
|
||||
cli_command = cli.cli_command
|
||||
os_info = util.get_os_info_ua()
|
||||
python_version = platform.python_version()
|
||||
ua = ua.format(certbot.__version__, cli_command, os_info,
|
||||
config.authenticator, config.installer, config.verb,
|
||||
ua_flags(config), platform.python_version(),
|
||||
ua_flags(config), python_version,
|
||||
"; " + config.user_agent_comment if config.user_agent_comment else "")
|
||||
else:
|
||||
ua = config.user_agent
|
||||
|
||||
@@ -37,6 +37,7 @@ CLI_DEFAULTS = dict(
|
||||
expand=False,
|
||||
renew_by_default=False,
|
||||
renew_with_new_domains=False,
|
||||
autorenew=True,
|
||||
allow_subset_of_names=False,
|
||||
tos=False,
|
||||
account=None,
|
||||
@@ -57,6 +58,7 @@ CLI_DEFAULTS = dict(
|
||||
rsa_key_size=2048,
|
||||
must_staple=False,
|
||||
redirect=None,
|
||||
auto_hsts=False,
|
||||
hsts=None,
|
||||
uir=None,
|
||||
staple=None,
|
||||
@@ -159,6 +161,7 @@ ACCOUNTS_DIR = "accounts"
|
||||
"""Directory where all accounts are saved."""
|
||||
|
||||
LE_REUSE_SERVERS = {
|
||||
'acme-v02.api.letsencrypt.org/directory': 'acme-v01.api.letsencrypt.org/directory',
|
||||
'acme-staging-v02.api.letsencrypt.org/directory':
|
||||
'acme-staging.api.letsencrypt.org/directory'
|
||||
}
|
||||
|
||||
@@ -29,6 +29,10 @@ HELP = "help"
|
||||
ESC = "esc"
|
||||
"""Display exit code when the user hits Escape (UNUSED)"""
|
||||
|
||||
# Display constants
|
||||
SIDE_FRAME = ("- " * 39) + "-"
|
||||
"""Display boundary (alternates spaces, so when copy-pasted, markdown doesn't interpret
|
||||
it as a heading)"""
|
||||
|
||||
def _wrap_lines(msg):
|
||||
"""Format lines nicely to 80 chars.
|
||||
@@ -111,12 +115,11 @@ class FileDisplay(object):
|
||||
because it won't cause any workflow regressions
|
||||
|
||||
"""
|
||||
side_frame = "-" * 79
|
||||
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))
|
||||
line=os.linesep, frame=SIDE_FRAME, msg=message))
|
||||
self.outfile.flush()
|
||||
if pause:
|
||||
if self._can_interact(force_interactive):
|
||||
@@ -208,12 +211,10 @@ class FileDisplay(object):
|
||||
if self._return_default(message, default, cli_flag, force_interactive):
|
||||
return default
|
||||
|
||||
side_frame = ("-" * 79) + os.linesep
|
||||
|
||||
message = _wrap_lines(message)
|
||||
|
||||
self.outfile.write("{0}{frame}{msg}{0}{frame}".format(
|
||||
os.linesep, frame=side_frame, msg=message))
|
||||
os.linesep, frame=SIDE_FRAME + os.linesep, msg=message))
|
||||
self.outfile.flush()
|
||||
|
||||
while True:
|
||||
@@ -386,8 +387,7 @@ class FileDisplay(object):
|
||||
# Write out the message to the user
|
||||
self.outfile.write(
|
||||
"{new}{msg}{new}".format(new=os.linesep, msg=message))
|
||||
side_frame = ("-" * 79) + os.linesep
|
||||
self.outfile.write(side_frame)
|
||||
self.outfile.write(SIDE_FRAME + os.linesep)
|
||||
|
||||
# Write out the menu choices
|
||||
for i, desc in enumerate(choices, 1):
|
||||
@@ -397,7 +397,7 @@ class FileDisplay(object):
|
||||
# Keep this outside of the textwrap
|
||||
self.outfile.write(os.linesep)
|
||||
|
||||
self.outfile.write(side_frame)
|
||||
self.outfile.write(SIDE_FRAME + os.linesep)
|
||||
self.outfile.flush()
|
||||
|
||||
def _get_valid_int_ans(self, max_):
|
||||
@@ -482,12 +482,11 @@ class NoninteractiveDisplay(object):
|
||||
:param bool wrap: Whether or not the application should wrap text
|
||||
|
||||
"""
|
||||
side_frame = "-" * 79
|
||||
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))
|
||||
line=os.linesep, frame=SIDE_FRAME, msg=message))
|
||||
self.outfile.flush()
|
||||
|
||||
def menu(self, message, choices, ok_label=None, cancel_label=None,
|
||||
|
||||
@@ -36,7 +36,7 @@ from certbot import util
|
||||
from certbot.display import util as display_util, ops as display_ops
|
||||
from certbot.plugins import disco as plugins_disco
|
||||
from certbot.plugins import selection as plug_sel
|
||||
|
||||
from certbot.plugins import enhancements
|
||||
|
||||
USER_CANCELLED = ("User chose to cancel the operation and may "
|
||||
"reinvoke the client.")
|
||||
@@ -473,8 +473,7 @@ def _report_new_cert(config, cert_path, fullchain_path, key_path=None):
|
||||
def _determine_account(config):
|
||||
"""Determine which account to use.
|
||||
|
||||
In order to make the renewer (configuration de/serialization) happy,
|
||||
if ``config.account`` is ``None``, it will be updated based on the
|
||||
If ``config.account`` is ``None``, it will be updated based on the
|
||||
user input. Same for ``config.email``.
|
||||
|
||||
:param config: Configuration object
|
||||
@@ -751,8 +750,8 @@ def _install_cert(config, le_client, domains, lineage=None):
|
||||
:param le_client: Client object
|
||||
:type le_client: client.Client
|
||||
|
||||
:param plugins: List of domains
|
||||
:type plugins: `list` of `str`
|
||||
:param domains: List of domains
|
||||
:type domains: `list` of `str`
|
||||
|
||||
:param lineage: Certificate lineage object. Defaults to `None`
|
||||
:type lineage: storage.RenewableCert
|
||||
@@ -790,11 +789,26 @@ def install(config, plugins):
|
||||
except errors.PluginSelectionError as e:
|
||||
return str(e)
|
||||
|
||||
custom_cert = (config.key_path and config.cert_path)
|
||||
if not config.certname and not custom_cert:
|
||||
certname_question = "Which certificate would you like to install?"
|
||||
config.certname = cert_manager.get_certnames(
|
||||
config, "install", allow_multiple=False,
|
||||
custom_prompt=certname_question)[0]
|
||||
|
||||
if not enhancements.are_supported(config, installer):
|
||||
raise errors.NotSupportedError("One ore more of the requested enhancements "
|
||||
"are not supported by the selected installer")
|
||||
# If cert-path is defined, populate missing (ie. not overridden) values.
|
||||
# Unfortunately this can't be done in argument parser, as certificate
|
||||
# manager needs the access to renewal directory paths
|
||||
if config.certname:
|
||||
config = _populate_from_certname(config)
|
||||
elif enhancements.are_requested(config):
|
||||
# Preflight config check
|
||||
raise errors.ConfigurationError("One or more of the requested enhancements "
|
||||
"require --cert-name to be provided")
|
||||
|
||||
if config.key_path and config.cert_path:
|
||||
_check_certificate_and_key(config)
|
||||
domains, _ = _find_domains_or_certname(config, installer)
|
||||
@@ -805,6 +819,11 @@ def install(config, plugins):
|
||||
"If your certificate is managed by Certbot, please use --cert-name "
|
||||
"to define which certificate you would like to install.")
|
||||
|
||||
if enhancements.are_requested(config):
|
||||
# In the case where we don't have certname, we have errored out already
|
||||
lineage = cert_manager.lineage_for_certname(config, config.certname)
|
||||
enhancements.enable(lineage, domains, installer, config)
|
||||
|
||||
def _populate_from_certname(config):
|
||||
"""Helper function for install to populate missing config values from lineage
|
||||
defined by --cert-name."""
|
||||
@@ -882,7 +901,8 @@ def enhance(config, plugins):
|
||||
"""
|
||||
supported_enhancements = ["hsts", "redirect", "uir", "staple"]
|
||||
# Check that at least one enhancement was requested on command line
|
||||
if not any([getattr(config, enh) for enh in supported_enhancements]):
|
||||
oldstyle_enh = any([getattr(config, enh) for enh in supported_enhancements])
|
||||
if not enhancements.are_requested(config) and not oldstyle_enh:
|
||||
msg = ("Please specify one or more enhancement types to configure. To list "
|
||||
"the available enhancement types, run:\n\n%s --help enhance\n")
|
||||
logger.warning(msg, sys.argv[0])
|
||||
@@ -893,6 +913,10 @@ def enhance(config, plugins):
|
||||
except errors.PluginSelectionError as e:
|
||||
return str(e)
|
||||
|
||||
if not enhancements.are_supported(config, installer):
|
||||
raise errors.NotSupportedError("One ore more of the requested enhancements "
|
||||
"are not supported by the selected installer")
|
||||
|
||||
certname_question = ("Which certificate would you like to use to enhance "
|
||||
"your configuration?")
|
||||
config.certname = cert_manager.get_certnames(
|
||||
@@ -908,11 +932,15 @@ def enhance(config, plugins):
|
||||
if not domains:
|
||||
raise errors.Error("User cancelled the domain selection. No domains "
|
||||
"defined, exiting.")
|
||||
if not config.chain_path:
|
||||
|
||||
lineage = cert_manager.lineage_for_certname(config, config.certname)
|
||||
if not config.chain_path:
|
||||
config.chain_path = lineage.chain_path
|
||||
if oldstyle_enh:
|
||||
le_client = _init_le_client(config, authenticator=None, installer=installer)
|
||||
le_client.enhance_config(domains, config.chain_path, ask_redirect=False)
|
||||
if enhancements.are_requested(config):
|
||||
enhancements.enable(lineage, domains, installer, config)
|
||||
|
||||
|
||||
def rollback(config, plugins):
|
||||
@@ -1074,6 +1102,11 @@ def run(config, plugins): # pylint: disable=too-many-branches,too-many-locals
|
||||
except errors.PluginSelectionError as e:
|
||||
return str(e)
|
||||
|
||||
# Preflight check for enhancement support by the selected installer
|
||||
if not enhancements.are_supported(config, installer):
|
||||
raise errors.NotSupportedError("One ore more of the requested enhancements "
|
||||
"are not supported by the selected installer")
|
||||
|
||||
# TODO: Handle errors from _init_le_client?
|
||||
le_client = _init_le_client(config, authenticator, installer)
|
||||
|
||||
@@ -1092,6 +1125,9 @@ def run(config, plugins): # pylint: disable=too-many-branches,too-many-locals
|
||||
|
||||
_install_cert(config, le_client, domains, new_lineage)
|
||||
|
||||
if enhancements.are_requested(config) and new_lineage:
|
||||
enhancements.enable(new_lineage, domains, installer, config)
|
||||
|
||||
if lineage is None or not should_get_cert:
|
||||
display_ops.success_installation(domains)
|
||||
else:
|
||||
|
||||
159
certbot/plugins/enhancements.py
Normal file
159
certbot/plugins/enhancements.py
Normal file
@@ -0,0 +1,159 @@
|
||||
"""New interface style Certbot enhancements"""
|
||||
import abc
|
||||
import six
|
||||
|
||||
from certbot import constants
|
||||
|
||||
from acme.magic_typing import Dict, List, Any # pylint: disable=unused-import, no-name-in-module
|
||||
|
||||
def enabled_enhancements(config):
|
||||
"""
|
||||
Generator to yield the enabled new style enhancements.
|
||||
|
||||
:param config: Configuration.
|
||||
:type config: :class:`certbot.interfaces.IConfig`
|
||||
"""
|
||||
for enh in _INDEX:
|
||||
if getattr(config, enh["cli_dest"]):
|
||||
yield enh
|
||||
|
||||
def are_requested(config):
|
||||
"""
|
||||
Checks if one or more of the requested enhancements are those of the new
|
||||
enhancement interfaces.
|
||||
|
||||
:param config: Configuration.
|
||||
:type config: :class:`certbot.interfaces.IConfig`
|
||||
"""
|
||||
return any(enabled_enhancements(config))
|
||||
|
||||
def are_supported(config, installer):
|
||||
"""
|
||||
Checks that all of the requested enhancements are supported by the
|
||||
installer.
|
||||
|
||||
:param config: Configuration.
|
||||
:type config: :class:`certbot.interfaces.IConfig`
|
||||
|
||||
:param installer: Installer object
|
||||
:type installer: interfaces.IInstaller
|
||||
|
||||
:returns: If all the requested enhancements are supported by the installer
|
||||
:rtype: bool
|
||||
"""
|
||||
for enh in enabled_enhancements(config):
|
||||
if not isinstance(installer, enh["class"]):
|
||||
return False
|
||||
return True
|
||||
|
||||
def enable(lineage, domains, installer, config):
|
||||
"""
|
||||
Run enable method for each requested enhancement that is supported.
|
||||
|
||||
:param lineage: Certificate lineage object
|
||||
:type lineage: certbot.storage.RenewableCert
|
||||
|
||||
:param domains: List of domains in certificate to enhance
|
||||
:type domains: str
|
||||
|
||||
:param installer: Installer object
|
||||
:type installer: interfaces.IInstaller
|
||||
|
||||
:param config: Configuration.
|
||||
:type config: :class:`certbot.interfaces.IConfig`
|
||||
"""
|
||||
for enh in enabled_enhancements(config):
|
||||
getattr(installer, enh["enable_function"])(lineage, domains)
|
||||
|
||||
def populate_cli(add):
|
||||
"""
|
||||
Populates the command line flags for certbot.cli.HelpfulParser
|
||||
|
||||
:param add: Add function of certbot.cli.HelpfulParser
|
||||
:type add: func
|
||||
"""
|
||||
for enh in _INDEX:
|
||||
add(enh["cli_groups"], enh["cli_flag"], action=enh["cli_action"],
|
||||
dest=enh["cli_dest"], default=enh["cli_flag_default"],
|
||||
help=enh["cli_help"])
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class AutoHSTSEnhancement(object):
|
||||
"""
|
||||
Enhancement interface that installer plugins can implement in order to
|
||||
provide functionality that configures the software to have a
|
||||
'Strict-Transport-Security' with initially low max-age value that will
|
||||
increase over time.
|
||||
|
||||
The plugins implementing new style enhancements are responsible of handling
|
||||
the saving of configuration checkpoints as well as calling possible restarts
|
||||
of managed software themselves.
|
||||
|
||||
Methods:
|
||||
enable_autohsts is called when the header is initially installed using a
|
||||
low max-age value.
|
||||
|
||||
update_autohsts is called every time when Certbot is run using 'renew'
|
||||
verb. The max-age value should be increased over time using this method.
|
||||
|
||||
deploy_autohsts is called for every lineage that has had its certificate
|
||||
renewed. A long HSTS max-age value should be set here, as we should be
|
||||
confident that the user is able to automatically renew their certificates.
|
||||
|
||||
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def update_autohsts(self, lineage, *args, **kwargs):
|
||||
"""
|
||||
Gets called for each lineage every time Certbot is run with 'renew' verb.
|
||||
Implementation of this method should increase the max-age value.
|
||||
|
||||
:param lineage: Certificate lineage object
|
||||
:type lineage: certbot.storage.RenewableCert
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def deploy_autohsts(self, lineage, *args, **kwargs):
|
||||
"""
|
||||
Gets called for a lineage when its certificate is successfully renewed.
|
||||
Long max-age value should be set in implementation of this method.
|
||||
|
||||
:param lineage: Certificate lineage object
|
||||
:type lineage: certbot.storage.RenewableCert
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def enable_autohsts(self, lineage, domains, *args, **kwargs):
|
||||
"""
|
||||
Enables the AutoHSTS enhancement, installing
|
||||
Strict-Transport-Security header with a low initial value to be increased
|
||||
over the subsequent runs of Certbot renew.
|
||||
|
||||
:param lineage: Certificate lineage object
|
||||
:type lineage: certbot.storage.RenewableCert
|
||||
|
||||
:param domains: List of domains in certificate to enhance
|
||||
:type domains: str
|
||||
"""
|
||||
|
||||
# This is used to configure internal new style enhancements in Certbot. These
|
||||
# enhancement interfaces need to be defined in this file. Please do not modify
|
||||
# this list from plugin code.
|
||||
_INDEX = [
|
||||
{
|
||||
"name": "AutoHSTS",
|
||||
"cli_help": "Gradually increasing max-age value for HTTP Strict Transport "+
|
||||
"Security security header",
|
||||
"cli_flag": "--auto-hsts",
|
||||
"cli_flag_default": constants.CLI_DEFAULTS["auto_hsts"],
|
||||
"cli_groups": ["security", "enhance"],
|
||||
"cli_dest": "auto_hsts",
|
||||
"cli_action": "store_true",
|
||||
"class": AutoHSTSEnhancement,
|
||||
"updater_function": "update_autohsts",
|
||||
"deployer_function": "deploy_autohsts",
|
||||
"enable_function": "enable_autohsts"
|
||||
}
|
||||
] # type: List[Dict[str, Any]]
|
||||
65
certbot/plugins/enhancements_test.py
Normal file
65
certbot/plugins/enhancements_test.py
Normal file
@@ -0,0 +1,65 @@
|
||||
"""Tests for new style enhancements"""
|
||||
import unittest
|
||||
import mock
|
||||
|
||||
from certbot.plugins import enhancements
|
||||
from certbot.plugins import null
|
||||
|
||||
import certbot.tests.util as test_util
|
||||
|
||||
|
||||
class EnhancementTest(test_util.ConfigTestCase):
|
||||
"""Tests for new style enhancements in certbot.plugins.enhancements"""
|
||||
|
||||
def setUp(self):
|
||||
super(EnhancementTest, self).setUp()
|
||||
self.mockinstaller = mock.MagicMock(spec=enhancements.AutoHSTSEnhancement)
|
||||
|
||||
|
||||
@test_util.patch_get_utility()
|
||||
def test_enhancement_enabled_enhancements(self, _):
|
||||
FAKEINDEX = [
|
||||
{
|
||||
"name": "autohsts",
|
||||
"cli_dest": "auto_hsts",
|
||||
},
|
||||
{
|
||||
"name": "somethingelse",
|
||||
"cli_dest": "something",
|
||||
}
|
||||
]
|
||||
with mock.patch("certbot.plugins.enhancements._INDEX", FAKEINDEX):
|
||||
self.config.auto_hsts = True
|
||||
self.config.something = True
|
||||
enabled = list(enhancements.enabled_enhancements(self.config))
|
||||
self.assertEqual(len(enabled), 2)
|
||||
self.assertTrue([i for i in enabled if i["name"] == "autohsts"])
|
||||
self.assertTrue([i for i in enabled if i["name"] == "somethingelse"])
|
||||
|
||||
def test_are_requested(self):
|
||||
self.assertEquals(
|
||||
len([i for i in enhancements.enabled_enhancements(self.config)]), 0)
|
||||
self.assertFalse(enhancements.are_requested(self.config))
|
||||
self.config.auto_hsts = True
|
||||
self.assertEquals(
|
||||
len([i for i in enhancements.enabled_enhancements(self.config)]), 1)
|
||||
self.assertTrue(enhancements.are_requested(self.config))
|
||||
|
||||
def test_are_supported(self):
|
||||
self.config.auto_hsts = True
|
||||
unsupported = null.Installer(self.config, "null")
|
||||
self.assertTrue(enhancements.are_supported(self.config, self.mockinstaller))
|
||||
self.assertFalse(enhancements.are_supported(self.config, unsupported))
|
||||
|
||||
def test_enable(self):
|
||||
self.config.auto_hsts = True
|
||||
domains = ["example.com", "www.example.com"]
|
||||
lineage = "lineage"
|
||||
enhancements.enable(lineage, domains, self.mockinstaller, self.config)
|
||||
self.assertTrue(self.mockinstaller.enable_autohsts.called)
|
||||
self.assertEquals(self.mockinstaller.enable_autohsts.call_args[0],
|
||||
(lineage, domains))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main() # pragma: no cover
|
||||
@@ -36,7 +36,8 @@ STR_CONFIG_ITEMS = ["config_dir", "logs_dir", "work_dir", "user_agent",
|
||||
"pre_hook", "post_hook", "tls_sni_01_address",
|
||||
"http01_address"]
|
||||
INT_CONFIG_ITEMS = ["rsa_key_size", "tls_sni_01_port", "http01_port"]
|
||||
BOOL_CONFIG_ITEMS = ["must_staple", "allow_subset_of_names", "reuse_key"]
|
||||
BOOL_CONFIG_ITEMS = ["must_staple", "allow_subset_of_names", "reuse_key",
|
||||
"autorenew"]
|
||||
|
||||
CONFIG_ITEMS = set(itertools.chain(
|
||||
BOOL_CONFIG_ITEMS, INT_CONFIG_ITEMS, STR_CONFIG_ITEMS, ('pref_challs',)))
|
||||
@@ -261,7 +262,7 @@ def should_renew(config, lineage):
|
||||
if config.renew_by_default:
|
||||
logger.debug("Auto-renewal forced with --force-renewal...")
|
||||
return True
|
||||
if lineage.should_autorenew(interactive=True):
|
||||
if lineage.should_autorenew():
|
||||
logger.info("Cert is due for renewal, auto-renewing...")
|
||||
return True
|
||||
if config.dry_run:
|
||||
|
||||
@@ -920,10 +920,10 @@ class RenewableCert(object):
|
||||
:rtype: bool
|
||||
|
||||
"""
|
||||
return ("autorenew" not in self.configuration or
|
||||
self.configuration.as_bool("autorenew"))
|
||||
return ("autorenew" not in self.configuration["renewalparams"] or
|
||||
self.configuration["renewalparams"].as_bool("autorenew"))
|
||||
|
||||
def should_autorenew(self, interactive=False):
|
||||
def should_autorenew(self):
|
||||
"""Should we now try to autorenew the most recent cert version?
|
||||
|
||||
This is a policy question and does not only depend on whether
|
||||
@@ -934,16 +934,12 @@ class RenewableCert(object):
|
||||
Note that this examines the numerically most recent cert version,
|
||||
not the currently deployed version.
|
||||
|
||||
:param bool interactive: set to True to examine the question
|
||||
regardless of whether the renewal configuration allows
|
||||
automated renewal (for interactive use). Default False.
|
||||
|
||||
:returns: whether an attempt should now be made to autorenew the
|
||||
most current cert version in this lineage
|
||||
:rtype: bool
|
||||
|
||||
"""
|
||||
if interactive or self.autorenewal_is_enabled():
|
||||
if self.autorenewal_is_enabled():
|
||||
# Consider whether to attempt to autorenew this cert now
|
||||
|
||||
# Renewals on the basis of revocation
|
||||
|
||||
@@ -218,12 +218,18 @@ class AccountFileStorageTest(test_util.ConfigTestCase):
|
||||
self._set_server('https://acme-staging.api.letsencrypt.org/directory')
|
||||
self.assertEqual([], self.storage.find_all())
|
||||
|
||||
def test_upgrade_version(self):
|
||||
def test_upgrade_version_staging(self):
|
||||
self._set_server('https://acme-staging.api.letsencrypt.org/directory')
|
||||
self.storage.save(self.acc, self.mock_client)
|
||||
self._set_server('https://acme-staging-v02.api.letsencrypt.org/directory')
|
||||
self.assertEqual([self.acc], self.storage.find_all())
|
||||
|
||||
def test_upgrade_version_production(self):
|
||||
self._set_server('https://acme-v01.api.letsencrypt.org/directory')
|
||||
self.storage.save(self.acc, self.mock_client)
|
||||
self._set_server('https://acme-v02.api.letsencrypt.org/directory')
|
||||
self.assertEqual([self.acc], self.storage.find_all())
|
||||
|
||||
@mock.patch('os.rmdir')
|
||||
def test_corrupted_account(self, mock_rmdir):
|
||||
# pylint: disable=protected-access
|
||||
@@ -235,6 +241,14 @@ class AccountFileStorageTest(test_util.ConfigTestCase):
|
||||
self._set_server('https://acme-staging-v02.api.letsencrypt.org/directory')
|
||||
self.assertEqual([], self.storage.find_all())
|
||||
|
||||
def test_upgrade_load(self):
|
||||
self._set_server('https://acme-staging.api.letsencrypt.org/directory')
|
||||
self.storage.save(self.acc, self.mock_client)
|
||||
prev_account = self.storage.load(self.acc.id)
|
||||
self._set_server('https://acme-staging-v02.api.letsencrypt.org/directory')
|
||||
account = self.storage.load(self.acc.id)
|
||||
self.assertEqual(prev_account, account)
|
||||
|
||||
def test_load_ioerror(self):
|
||||
self.storage.save(self.acc, self.mock_client)
|
||||
mock_open = mock.mock_open()
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Tests for certbot.client."""
|
||||
import os
|
||||
import platform
|
||||
import shutil
|
||||
import tempfile
|
||||
import unittest
|
||||
@@ -16,6 +17,38 @@ KEY = test_util.load_vector("rsa512_key.pem")
|
||||
CSR_SAN = test_util.load_vector("csr-san_512.pem")
|
||||
|
||||
|
||||
class DetermineUserAgentTest(test_util.ConfigTestCase):
|
||||
"""Tests for certbot.client.determine_user_agent."""
|
||||
|
||||
def _call(self):
|
||||
from certbot.client import determine_user_agent
|
||||
return determine_user_agent(self.config)
|
||||
|
||||
@mock.patch.dict(os.environ, {"CERTBOT_DOCS": "1"})
|
||||
def test_docs_value(self):
|
||||
self._test(expect_doc_values=True)
|
||||
|
||||
@mock.patch.dict(os.environ, {})
|
||||
def test_real_values(self):
|
||||
self._test(expect_doc_values=False)
|
||||
|
||||
def _test(self, expect_doc_values):
|
||||
ua = self._call()
|
||||
|
||||
if expect_doc_values:
|
||||
doc_value_check = self.assertIn
|
||||
real_value_check = self.assertNotIn
|
||||
else:
|
||||
doc_value_check = self.assertNotIn
|
||||
real_value_check = self.assertIn
|
||||
|
||||
doc_value_check("certbot(-auto)", ua)
|
||||
doc_value_check("OS_NAME OS_VERSION", ua)
|
||||
doc_value_check("major.minor.patchlevel", ua)
|
||||
real_value_check(util.get_os_info_ua(), ua)
|
||||
real_value_check(platform.python_version(), ua)
|
||||
|
||||
|
||||
class RegisterTest(test_util.ConfigTestCase):
|
||||
"""Tests for certbot.client.register."""
|
||||
|
||||
|
||||
@@ -29,7 +29,9 @@ from certbot import updater
|
||||
from certbot import util
|
||||
|
||||
from certbot.plugins import disco
|
||||
from certbot.plugins import enhancements
|
||||
from certbot.plugins import manual
|
||||
from certbot.plugins import null
|
||||
|
||||
import certbot.tests.util as test_util
|
||||
|
||||
@@ -52,10 +54,11 @@ class TestHandleIdenticalCerts(unittest.TestCase):
|
||||
self.assertEqual(ret, ("reinstall", mock_lineage))
|
||||
|
||||
|
||||
class RunTest(unittest.TestCase):
|
||||
class RunTest(test_util.ConfigTestCase):
|
||||
"""Tests for certbot.main.run."""
|
||||
|
||||
def setUp(self):
|
||||
super(RunTest, self).setUp()
|
||||
self.domain = 'example.org'
|
||||
self.patches = [
|
||||
mock.patch('certbot.main._get_and_save_cert'),
|
||||
@@ -105,6 +108,15 @@ class RunTest(unittest.TestCase):
|
||||
self._call()
|
||||
self.mock_success_renewal.assert_called_once_with([self.domain])
|
||||
|
||||
@mock.patch('certbot.main.plug_sel.choose_configurator_plugins')
|
||||
def test_run_enhancement_not_supported(self, mock_choose):
|
||||
mock_choose.return_value = (null.Installer(self.config, "null"), None)
|
||||
plugins = disco.PluginsRegistry.find_all()
|
||||
self.config.auto_hsts = True
|
||||
self.assertRaises(errors.NotSupportedError,
|
||||
main.run,
|
||||
self.config, plugins)
|
||||
|
||||
|
||||
class CertonlyTest(unittest.TestCase):
|
||||
"""Tests for certbot.main.certonly."""
|
||||
@@ -749,20 +761,25 @@ class MainTest(test_util.ConfigTestCase): # pylint: disable=too-many-public-met
|
||||
@mock.patch('certbot.main.plug_sel.record_chosen_plugins')
|
||||
@mock.patch('certbot.main.plug_sel.pick_installer')
|
||||
def test_installer_param_error(self, _inst, _rec):
|
||||
self.assertRaises(errors.ConfigurationError,
|
||||
self._call,
|
||||
['install', '--key-path', '/tmp/key_path'])
|
||||
self.assertRaises(errors.ConfigurationError,
|
||||
self._call,
|
||||
['install', '--cert-path', '/tmp/key_path'])
|
||||
self.assertRaises(errors.ConfigurationError,
|
||||
self._call,
|
||||
['install'])
|
||||
self.assertRaises(errors.ConfigurationError,
|
||||
self._call,
|
||||
['install', '--cert-name', 'notfound',
|
||||
'--key-path', 'invalid'])
|
||||
|
||||
@mock.patch('certbot.main.plug_sel.record_chosen_plugins')
|
||||
@mock.patch('certbot.main.plug_sel.pick_installer')
|
||||
@mock.patch('certbot.cert_manager.get_certnames')
|
||||
@mock.patch('certbot.main._install_cert')
|
||||
def test_installer_select_cert(self, mock_inst, mock_getcert, _inst, _rec):
|
||||
mock_lineage = mock.MagicMock(cert_path="/tmp/cert", chain_path="/tmp/chain",
|
||||
fullchain_path="/tmp/chain",
|
||||
key_path="/tmp/privkey")
|
||||
with mock.patch("certbot.cert_manager.lineage_for_certname") as mock_getlin:
|
||||
mock_getlin.return_value = mock_lineage
|
||||
self._call(['install'], mockisfile=True)
|
||||
self.assertTrue(mock_getcert.called)
|
||||
self.assertTrue(mock_inst.called)
|
||||
|
||||
@mock.patch('certbot.main._report_new_cert')
|
||||
@mock.patch('certbot.util.exe_exists')
|
||||
def test_configurator_selection(self, mock_exe_exists, unused_report):
|
||||
@@ -1573,12 +1590,14 @@ class MakeOrVerifyNeededDirs(test_util.ConfigTestCase):
|
||||
strict=self.config.strict_permissions)
|
||||
|
||||
|
||||
class EnhanceTest(unittest.TestCase):
|
||||
class EnhanceTest(test_util.ConfigTestCase):
|
||||
"""Tests for certbot.main.enhance."""
|
||||
|
||||
def setUp(self):
|
||||
super(EnhanceTest, self).setUp()
|
||||
self.get_utility_patch = test_util.patch_get_utility()
|
||||
self.mock_get_utility = self.get_utility_patch.start()
|
||||
self.mockinstaller = mock.MagicMock(spec=enhancements.AutoHSTSEnhancement)
|
||||
|
||||
def tearDown(self):
|
||||
self.get_utility_patch.stop()
|
||||
@@ -1670,7 +1689,7 @@ class EnhanceTest(unittest.TestCase):
|
||||
|
||||
def test_no_enhancements_defined(self):
|
||||
self.assertRaises(errors.MisconfigurationError,
|
||||
self._call, ['enhance'])
|
||||
self._call, ['enhance', '-a', 'null'])
|
||||
|
||||
@mock.patch('certbot.main.plug_sel.choose_configurator_plugins')
|
||||
@mock.patch('certbot.main.display_ops.choose_values')
|
||||
@@ -1682,5 +1701,70 @@ class EnhanceTest(unittest.TestCase):
|
||||
mock_client = self._call(['enhance', '--hsts'])
|
||||
self.assertFalse(mock_client.enhance_config.called)
|
||||
|
||||
@mock.patch('certbot.cert_manager.lineage_for_certname')
|
||||
@mock.patch('certbot.main.display_ops.choose_values')
|
||||
@mock.patch('certbot.main.plug_sel.pick_installer')
|
||||
@mock.patch('certbot.main.plug_sel.record_chosen_plugins')
|
||||
@test_util.patch_get_utility()
|
||||
def test_enhancement_enable(self, _, _rec, mock_inst, mock_choose, mock_lineage):
|
||||
mock_inst.return_value = self.mockinstaller
|
||||
mock_choose.return_value = ["example.com", "another.tld"]
|
||||
mock_lineage.return_value = mock.MagicMock(chain_path="/tmp/nonexistent")
|
||||
self._call(['enhance', '--auto-hsts'])
|
||||
self.assertTrue(self.mockinstaller.enable_autohsts.called)
|
||||
self.assertEquals(self.mockinstaller.enable_autohsts.call_args[0][1],
|
||||
["example.com", "another.tld"])
|
||||
|
||||
@mock.patch('certbot.cert_manager.lineage_for_certname')
|
||||
@mock.patch('certbot.main.display_ops.choose_values')
|
||||
@mock.patch('certbot.main.plug_sel.pick_installer')
|
||||
@mock.patch('certbot.main.plug_sel.record_chosen_plugins')
|
||||
@test_util.patch_get_utility()
|
||||
def test_enhancement_enable_not_supported(self, _, _rec, mock_inst, mock_choose, mock_lineage):
|
||||
mock_inst.return_value = null.Installer(self.config, "null")
|
||||
mock_choose.return_value = ["example.com", "another.tld"]
|
||||
mock_lineage.return_value = mock.MagicMock(chain_path="/tmp/nonexistent")
|
||||
self.assertRaises(
|
||||
errors.NotSupportedError,
|
||||
self._call, ['enhance', '--auto-hsts'])
|
||||
|
||||
def test_enhancement_enable_conflict(self):
|
||||
self.assertRaises(
|
||||
errors.Error,
|
||||
self._call, ['enhance', '--auto-hsts', '--hsts'])
|
||||
|
||||
|
||||
class InstallTest(test_util.ConfigTestCase):
|
||||
"""Tests for certbot.main.install."""
|
||||
|
||||
def setUp(self):
|
||||
super(InstallTest, self).setUp()
|
||||
self.mockinstaller = mock.MagicMock(spec=enhancements.AutoHSTSEnhancement)
|
||||
|
||||
@mock.patch('certbot.main.plug_sel.record_chosen_plugins')
|
||||
@mock.patch('certbot.main.plug_sel.pick_installer')
|
||||
def test_install_enhancement_not_supported(self, mock_inst, _rec):
|
||||
mock_inst.return_value = null.Installer(self.config, "null")
|
||||
plugins = disco.PluginsRegistry.find_all()
|
||||
self.config.auto_hsts = True
|
||||
self.config.certname = "nonexistent"
|
||||
self.assertRaises(errors.NotSupportedError,
|
||||
main.install,
|
||||
self.config, plugins)
|
||||
|
||||
@mock.patch('certbot.main.plug_sel.record_chosen_plugins')
|
||||
@mock.patch('certbot.main.plug_sel.pick_installer')
|
||||
def test_install_enhancement_no_certname(self, mock_inst, _rec):
|
||||
mock_inst.return_value = self.mockinstaller
|
||||
plugins = disco.PluginsRegistry.find_all()
|
||||
self.config.auto_hsts = True
|
||||
self.config.certname = None
|
||||
self.config.key_path = "/tmp/nonexistent"
|
||||
self.config.cert_path = "/tmp/nonexistent"
|
||||
self.assertRaises(errors.ConfigurationError,
|
||||
main.install,
|
||||
self.config, plugins)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main() # pragma: no cover
|
||||
|
||||
@@ -6,6 +6,8 @@ from certbot import interfaces
|
||||
from certbot import main
|
||||
from certbot import updater
|
||||
|
||||
from certbot.plugins import enhancements
|
||||
|
||||
import certbot.tests.util as test_util
|
||||
|
||||
|
||||
@@ -14,25 +16,10 @@ class RenewUpdaterTest(test_util.ConfigTestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(RenewUpdaterTest, self).setUp()
|
||||
class MockInstallerGenericUpdater(interfaces.GenericUpdater):
|
||||
"""Mock class that implements GenericUpdater"""
|
||||
def __init__(self, *args, **kwargs):
|
||||
# pylint: disable=unused-argument
|
||||
self.restart = mock.MagicMock()
|
||||
self.callcounter = mock.MagicMock()
|
||||
def generic_updates(self, lineage, *args, **kwargs):
|
||||
self.callcounter(*args, **kwargs)
|
||||
|
||||
class MockInstallerRenewDeployer(interfaces.RenewDeployer):
|
||||
"""Mock class that implements RenewDeployer"""
|
||||
def __init__(self, *args, **kwargs):
|
||||
# pylint: disable=unused-argument
|
||||
self.callcounter = mock.MagicMock()
|
||||
def renew_deploy(self, lineage, *args, **kwargs):
|
||||
self.callcounter(*args, **kwargs)
|
||||
|
||||
self.generic_updater = MockInstallerGenericUpdater()
|
||||
self.renew_deployer = MockInstallerRenewDeployer()
|
||||
self.generic_updater = mock.MagicMock(spec=interfaces.GenericUpdater)
|
||||
self.generic_updater.restart = mock.MagicMock()
|
||||
self.renew_deployer = mock.MagicMock(spec=interfaces.RenewDeployer)
|
||||
self.mockinstaller = mock.MagicMock(spec=enhancements.AutoHSTSEnhancement)
|
||||
|
||||
@mock.patch('certbot.main._get_and_save_cert')
|
||||
@mock.patch('certbot.plugins.selection.choose_configurator_plugins')
|
||||
@@ -48,16 +35,16 @@ class RenewUpdaterTest(test_util.ConfigTestCase):
|
||||
self.assertTrue(mock_generic_updater.restart.called)
|
||||
|
||||
mock_generic_updater.restart.reset_mock()
|
||||
mock_generic_updater.callcounter.reset_mock()
|
||||
mock_generic_updater.generic_updates.reset_mock()
|
||||
updater.run_generic_updaters(self.config, mock.MagicMock(), None)
|
||||
self.assertEqual(mock_generic_updater.callcounter.call_count, 1)
|
||||
self.assertEqual(mock_generic_updater.generic_updates.call_count, 1)
|
||||
self.assertFalse(mock_generic_updater.restart.called)
|
||||
|
||||
def test_renew_deployer(self):
|
||||
lineage = mock.MagicMock()
|
||||
mock_deployer = self.renew_deployer
|
||||
updater.run_renewal_deployer(self.config, lineage, mock_deployer)
|
||||
self.assertTrue(mock_deployer.callcounter.called_with(lineage))
|
||||
self.assertTrue(mock_deployer.renew_deploy.called_with(lineage))
|
||||
|
||||
@mock.patch("certbot.updater.logger.debug")
|
||||
def test_updater_skip_dry_run(self, mock_log):
|
||||
@@ -75,6 +62,62 @@ class RenewUpdaterTest(test_util.ConfigTestCase):
|
||||
self.assertEquals(mock_log.call_args[0][0],
|
||||
"Skipping renewal deployer in dry-run mode.")
|
||||
|
||||
@mock.patch('certbot.plugins.selection.choose_configurator_plugins')
|
||||
def test_enhancement_updates(self, mock_select):
|
||||
mock_select.return_value = (self.mockinstaller, None)
|
||||
updater.run_generic_updaters(self.config, mock.MagicMock(), None)
|
||||
self.assertTrue(self.mockinstaller.update_autohsts.called)
|
||||
self.assertEqual(self.mockinstaller.update_autohsts.call_count, 1)
|
||||
|
||||
def test_enhancement_deployer(self):
|
||||
updater.run_renewal_deployer(self.config, mock.MagicMock(),
|
||||
self.mockinstaller)
|
||||
self.assertTrue(self.mockinstaller.deploy_autohsts.called)
|
||||
|
||||
@mock.patch('certbot.plugins.selection.choose_configurator_plugins')
|
||||
def test_enhancement_updates_not_called(self, mock_select):
|
||||
self.config.disable_renew_updates = True
|
||||
mock_select.return_value = (self.mockinstaller, None)
|
||||
updater.run_generic_updaters(self.config, mock.MagicMock(), None)
|
||||
self.assertFalse(self.mockinstaller.update_autohsts.called)
|
||||
|
||||
def test_enhancement_deployer_not_called(self):
|
||||
self.config.disable_renew_updates = True
|
||||
updater.run_renewal_deployer(self.config, mock.MagicMock(),
|
||||
self.mockinstaller)
|
||||
self.assertFalse(self.mockinstaller.deploy_autohsts.called)
|
||||
|
||||
@mock.patch('certbot.plugins.selection.choose_configurator_plugins')
|
||||
def test_enhancement_no_updater(self, mock_select):
|
||||
FAKEINDEX = [
|
||||
{
|
||||
"name": "Test",
|
||||
"class": enhancements.AutoHSTSEnhancement,
|
||||
"updater_function": None,
|
||||
"deployer_function": "deploy_autohsts",
|
||||
"enable_function": "enable_autohsts"
|
||||
}
|
||||
]
|
||||
mock_select.return_value = (self.mockinstaller, None)
|
||||
with mock.patch("certbot.plugins.enhancements._INDEX", FAKEINDEX):
|
||||
updater.run_generic_updaters(self.config, mock.MagicMock(), None)
|
||||
self.assertFalse(self.mockinstaller.update_autohsts.called)
|
||||
|
||||
def test_enhancement_no_deployer(self):
|
||||
FAKEINDEX = [
|
||||
{
|
||||
"name": "Test",
|
||||
"class": enhancements.AutoHSTSEnhancement,
|
||||
"updater_function": "deploy_autohsts",
|
||||
"deployer_function": None,
|
||||
"enable_function": "enable_autohsts"
|
||||
}
|
||||
]
|
||||
with mock.patch("certbot.plugins.enhancements._INDEX", FAKEINDEX):
|
||||
updater.run_renewal_deployer(self.config, mock.MagicMock(),
|
||||
self.mockinstaller)
|
||||
self.assertFalse(self.mockinstaller.deploy_autohsts.called)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main() # pragma: no cover
|
||||
|
||||
@@ -383,8 +383,9 @@ class RenewableCertTests(BaseRenewableCertTest):
|
||||
os.unlink(self.test_rc.cert)
|
||||
self.assertRaises(errors.CertStorageError, self.test_rc.names)
|
||||
|
||||
@mock.patch("certbot.storage.cli")
|
||||
@mock.patch("certbot.storage.datetime")
|
||||
def test_time_interval_judgments(self, mock_datetime):
|
||||
def test_time_interval_judgments(self, mock_datetime, mock_cli):
|
||||
"""Test should_autodeploy() and should_autorenew() on the basis
|
||||
of expiry time windows."""
|
||||
test_cert = test_util.load_vector("cert_512.pem")
|
||||
@@ -399,6 +400,8 @@ class RenewableCertTests(BaseRenewableCertTest):
|
||||
f.write(test_cert)
|
||||
|
||||
mock_datetime.timedelta = datetime.timedelta
|
||||
mock_cli.set_by_cli.return_value = False
|
||||
self.test_rc.configuration["renewalparams"] = {}
|
||||
|
||||
for (current_time, interval, result) in [
|
||||
# 2014-12-13 12:00:00+00:00 (about 5 days prior to expiry)
|
||||
@@ -451,22 +454,25 @@ class RenewableCertTests(BaseRenewableCertTest):
|
||||
self.assertFalse(self.test_rc.should_autodeploy())
|
||||
|
||||
def test_autorenewal_is_enabled(self):
|
||||
self.test_rc.configuration["renewalparams"] = {}
|
||||
self.assertTrue(self.test_rc.autorenewal_is_enabled())
|
||||
self.test_rc.configuration["autorenew"] = "1"
|
||||
self.test_rc.configuration["renewalparams"]["autorenew"] = "True"
|
||||
self.assertTrue(self.test_rc.autorenewal_is_enabled())
|
||||
|
||||
self.test_rc.configuration["autorenew"] = "0"
|
||||
self.test_rc.configuration["renewalparams"]["autorenew"] = "False"
|
||||
self.assertFalse(self.test_rc.autorenewal_is_enabled())
|
||||
|
||||
@mock.patch("certbot.storage.cli")
|
||||
@mock.patch("certbot.storage.RenewableCert.ocsp_revoked")
|
||||
def test_should_autorenew(self, mock_ocsp):
|
||||
def test_should_autorenew(self, mock_ocsp, mock_cli):
|
||||
"""Test should_autorenew on the basis of reasons other than
|
||||
expiry time window."""
|
||||
# pylint: disable=too-many-statements
|
||||
mock_cli.set_by_cli.return_value = False
|
||||
# Autorenewal turned off
|
||||
self.test_rc.configuration["autorenew"] = "0"
|
||||
self.test_rc.configuration["renewalparams"] = {"autorenew": "False"}
|
||||
self.assertFalse(self.test_rc.should_autorenew())
|
||||
self.test_rc.configuration["autorenew"] = "1"
|
||||
self.test_rc.configuration["renewalparams"]["autorenew"] = "True"
|
||||
for kind in ALL_FOUR:
|
||||
self._write_out_kind(kind, 12)
|
||||
# Mandatory renewal on the basis of OCSP revocation
|
||||
|
||||
@@ -5,6 +5,7 @@ from certbot import errors
|
||||
from certbot import interfaces
|
||||
|
||||
from certbot.plugins import selection as plug_sel
|
||||
import certbot.plugins.enhancements as enhancements
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -33,6 +34,7 @@ def run_generic_updaters(config, lineage, plugins):
|
||||
logger.warning("Could not choose appropriate plugin for updaters: %s", e)
|
||||
return
|
||||
_run_updaters(lineage, installer, config)
|
||||
_run_enhancement_updaters(lineage, installer, config)
|
||||
|
||||
def run_renewal_deployer(config, lineage, installer):
|
||||
"""Helper function to run deployer interface method if supported by the used
|
||||
@@ -57,6 +59,7 @@ def run_renewal_deployer(config, lineage, installer):
|
||||
if not config.disable_renew_updates and isinstance(installer,
|
||||
interfaces.RenewDeployer):
|
||||
installer.renew_deploy(lineage)
|
||||
_run_enhancement_deployers(lineage, installer, config)
|
||||
|
||||
def _run_updaters(lineage, installer, config):
|
||||
"""Helper function to run the updater interface methods if supported by the
|
||||
@@ -74,3 +77,46 @@ def _run_updaters(lineage, installer, config):
|
||||
if not config.disable_renew_updates:
|
||||
if isinstance(installer, interfaces.GenericUpdater):
|
||||
installer.generic_updates(lineage)
|
||||
|
||||
def _run_enhancement_updaters(lineage, installer, config):
|
||||
"""Iterates through known enhancement interfaces. If the installer implements
|
||||
an enhancement interface and the enhance interface has an updater method, the
|
||||
updater method gets run.
|
||||
|
||||
:param lineage: Certificate lineage object
|
||||
:type lineage: storage.RenewableCert
|
||||
|
||||
:param installer: Installer object
|
||||
:type installer: interfaces.IInstaller
|
||||
|
||||
:param config: Configuration object
|
||||
:type config: interfaces.IConfig
|
||||
"""
|
||||
|
||||
if config.disable_renew_updates:
|
||||
return
|
||||
for enh in enhancements._INDEX: # pylint: disable=protected-access
|
||||
if isinstance(installer, enh["class"]) and enh["updater_function"]:
|
||||
getattr(installer, enh["updater_function"])(lineage)
|
||||
|
||||
|
||||
def _run_enhancement_deployers(lineage, installer, config):
|
||||
"""Iterates through known enhancement interfaces. If the installer implements
|
||||
an enhancement interface and the enhance interface has an deployer method, the
|
||||
deployer method gets run.
|
||||
|
||||
:param lineage: Certificate lineage object
|
||||
:type lineage: storage.RenewableCert
|
||||
|
||||
:param installer: Installer object
|
||||
:type installer: interfaces.IInstaller
|
||||
|
||||
:param config: Configuration object
|
||||
:type config: interfaces.IConfig
|
||||
"""
|
||||
|
||||
if config.disable_renew_updates:
|
||||
return
|
||||
for enh in enhancements._INDEX: # pylint: disable=protected-access
|
||||
if isinstance(installer, enh["class"]) and enh["deployer_function"]:
|
||||
getattr(installer, enh["deployer_function"])(lineage)
|
||||
|
||||
@@ -108,12 +108,12 @@ optional arguments:
|
||||
case, and to know when to deprecate support for past
|
||||
Python versions and flags. If you wish to hide this
|
||||
information from the Let's Encrypt server, set this to
|
||||
"". (default: CertbotACMEClient/0.24.0 (certbot;
|
||||
darwin 10.13.4) Authenticator/XXX Installer/YYY
|
||||
(SUBCOMMAND; flags: FLAGS) Py/2.7.14). The flags
|
||||
encoded in the user agent are: --duplicate, --force-
|
||||
renew, --allow-subset-of-names, -n, and whether any
|
||||
hooks are set.
|
||||
"". (default: CertbotACMEClient/0.25.1
|
||||
(certbot(-auto); OS_NAME OS_VERSION) Authenticator/XXX
|
||||
Installer/YYY (SUBCOMMAND; flags: FLAGS)
|
||||
Py/major.minor.patchlevel). The flags encoded in the
|
||||
user agent are: --duplicate, --force-renew, --allow-
|
||||
subset-of-names, -n, and whether any hooks are set.
|
||||
--user-agent-comment USER_AGENT_COMMENT
|
||||
Add a comment to the default user agent string. May be
|
||||
used when repackaging Certbot or calling it from
|
||||
@@ -143,6 +143,8 @@ automation:
|
||||
certificate name but does not match the requested
|
||||
domains, renew it now, regardless of whether it is
|
||||
near expiry. (default: False)
|
||||
--reuse-key When renewing, use the same private key as the
|
||||
existing certificate. (default: False)
|
||||
--allow-subset-of-names
|
||||
When performing domain validation, do not consider it
|
||||
a failure if authorizations can not be obtained for a
|
||||
@@ -319,6 +321,13 @@ renew:
|
||||
disable it. (default: False)
|
||||
--no-directory-hooks Disable running executables found in Certbot's hook
|
||||
directories during renewal. (default: False)
|
||||
--disable-renew-updates
|
||||
Disable automatic updates to your server configuration
|
||||
that would otherwise be done by the selected installer
|
||||
plugin, and triggered when the user executes "certbot
|
||||
renew", regardless of if the certificate is renewed.
|
||||
This setting does not apply to important TLS
|
||||
configuration updates. (default: False)
|
||||
|
||||
certificates:
|
||||
List certificates managed by Certbot
|
||||
@@ -360,8 +369,9 @@ register:
|
||||
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)
|
||||
Email used for registration and recovery contact. Use
|
||||
comma to register multiple emails, ex:
|
||||
u1@example.com,u2@example.com. (default: Ask).
|
||||
--eff-email Share your e-mail address with EFF (default: None)
|
||||
--no-eff-email Don't share your e-mail address with EFF (default:
|
||||
None)
|
||||
@@ -399,7 +409,7 @@ update_symlinks:
|
||||
changed them by hand or edited a renewal configuration file
|
||||
|
||||
enhance:
|
||||
Helps to harden the TLS configration by adding security enhancements to
|
||||
Helps to harden the TLS configuration by adding security enhancements to
|
||||
already existing configuration.
|
||||
|
||||
plugins:
|
||||
@@ -472,9 +482,9 @@ apache:
|
||||
/etc/apache2/other)
|
||||
--apache-handle-modules APACHE_HANDLE_MODULES
|
||||
Let installer handle enabling required modules for
|
||||
you.(Only Ubuntu/Debian currently) (default: False)
|
||||
you. (Only Ubuntu/Debian currently) (default: False)
|
||||
--apache-handle-sites APACHE_HANDLE_SITES
|
||||
Let installer handle enabling sites for you.(Only
|
||||
Let installer handle enabling sites for you. (Only
|
||||
Ubuntu/Debian currently) (default: False)
|
||||
|
||||
certbot-route53:auth:
|
||||
@@ -628,7 +638,8 @@ nginx:
|
||||
Nginx Web Server plugin - Alpha
|
||||
|
||||
--nginx-server-root NGINX_SERVER_ROOT
|
||||
Nginx server root directory. (default: /etc/nginx)
|
||||
Nginx server root directory. (default: /etc/nginx or
|
||||
/usr/local/etc/nginx)
|
||||
--nginx-ctl NGINX_CTL
|
||||
Path to the 'nginx' binary, used for 'configtest' and
|
||||
retrieving nginx version number. (default: nginx)
|
||||
|
||||
@@ -454,6 +454,12 @@ Renewing certificates
|
||||
days). Make sure you renew the certificates at least once in 3
|
||||
months.
|
||||
|
||||
.. seealso:: Many of the certbot clients obtained through a
|
||||
distribution come with automatic renewal out of the box,
|
||||
such as Debian and Ubuntu versions installed through `apt`,
|
||||
CentOS/RHEL 7 through EPEL, etc. See `Automated Renewals`_
|
||||
for more details.
|
||||
|
||||
As of version 0.10.0, Certbot supports a ``renew`` action to check
|
||||
all installed certificates for impending expiry and attempt to renew
|
||||
them. The simplest form is simply
|
||||
@@ -560,12 +566,6 @@ can run on a regular basis, like every week or every day). In that case,
|
||||
you are likely to want to use the ``-q`` or ``--quiet`` quiet flag to
|
||||
silence all output except errors.
|
||||
|
||||
.. seealso:: Many of the certbot clients obtained through a
|
||||
distribution come with automatic renewal out of the box,
|
||||
such as Debian and Ubuntu versions installed through `apt`,
|
||||
CentOS/RHEL 7 through EPEL, etc. See `Automated Renewals`_
|
||||
for more details.
|
||||
|
||||
If you are manually renewing all of your certificates, the
|
||||
``--force-renewal`` flag may be helpful; it causes the expiration time of
|
||||
the certificate(s) to be ignored when considering renewal, and attempts to
|
||||
|
||||
@@ -31,7 +31,7 @@ if [ -z "$VENV_PATH" ]; then
|
||||
fi
|
||||
VENV_BIN="$VENV_PATH/bin"
|
||||
BOOTSTRAP_VERSION_PATH="$VENV_PATH/certbot-auto-bootstrap-version.txt"
|
||||
LE_AUTO_VERSION="0.24.0"
|
||||
LE_AUTO_VERSION="0.25.1"
|
||||
BASENAME=$(basename $0)
|
||||
USAGE="Usage: $BASENAME [OPTIONS]
|
||||
A self-updating wrapper script for the Certbot ACME client. When run, updates
|
||||
@@ -1055,9 +1055,11 @@ cffi==1.10.0 \
|
||||
--hash=sha256:5576644b859197da7bbd8f8c7c2fb5dcc6cd505cadb42992d5f104c013f8a214 \
|
||||
--hash=sha256:b3b02911eb1f6ada203b0763ba924234629b51586f72a21faacc638269f4ced5
|
||||
ConfigArgParse==0.12.0 \
|
||||
--hash=sha256:28cd7d67669651f2a4518367838c49539457504584a139709b2b8f6c208ef339
|
||||
--hash=sha256:28cd7d67669651f2a4518367838c49539457504584a139709b2b8f6c208ef339 \
|
||||
--no-binary ConfigArgParse
|
||||
configobj==5.0.6 \
|
||||
--hash=sha256:a2f5650770e1c87fb335af19a9b7eb73fc05ccf22144eb68db7d00cd2bcb0902
|
||||
--hash=sha256:a2f5650770e1c87fb335af19a9b7eb73fc05ccf22144eb68db7d00cd2bcb0902 \
|
||||
--no-binary configobj
|
||||
cryptography==2.0.2 \
|
||||
--hash=sha256:187ae17358436d2c760f28c2aeb02fefa3f37647a9c5b6f7f7c3e83cd1c5a972 \
|
||||
--hash=sha256:19e43a13bbf52028dd1e810c803f2ad8880d0692d772f98d42e1eaf34bdee3d6 \
|
||||
@@ -1112,7 +1114,8 @@ mock==1.3.0 \
|
||||
--hash=sha256:3f573a18be94de886d1191f27c168427ef693e8dcfcecf95b170577b2eb69cbb \
|
||||
--hash=sha256:1e247dbecc6ce057299eb7ee019ad68314bb93152e81d9a6110d35f4d5eca0f6
|
||||
ordereddict==1.1 \
|
||||
--hash=sha256:1c35b4ac206cef2d24816c89f89cf289dd3d38cf7c449bb3fab7bf6d43f01b1f
|
||||
--hash=sha256:1c35b4ac206cef2d24816c89f89cf289dd3d38cf7c449bb3fab7bf6d43f01b1f \
|
||||
--no-binary ordereddict
|
||||
packaging==16.8 \
|
||||
--hash=sha256:99276dc6e3a7851f32027a68f1095cd3f77c148091b092ea867a351811cfe388 \
|
||||
--hash=sha256:5d50835fdf0a7edf0b55e311b7c887786504efea1177abd7e69329a8e5ea619e
|
||||
@@ -1138,7 +1141,8 @@ pyRFC3339==1.0 \
|
||||
--hash=sha256:eea31835c56e2096af4363a5745a784878a61d043e247d3a6d6a0a32a9741f56 \
|
||||
--hash=sha256:8dfbc6c458b8daba1c0f3620a8c78008b323a268b27b7359e92a4ae41325f535
|
||||
python-augeas==0.5.0 \
|
||||
--hash=sha256:67d59d66cdba8d624e0389b87b2a83a176f21f16a87553b50f5703b23f29bac2
|
||||
--hash=sha256:67d59d66cdba8d624e0389b87b2a83a176f21f16a87553b50f5703b23f29bac2 \
|
||||
--no-binary python-augeas
|
||||
pytz==2015.7 \
|
||||
--hash=sha256:3abe6a6d3fc2fbbe4c60144211f45da2edbe3182a6f6511af6bbba0598b1f992 \
|
||||
--hash=sha256:939ef9c1e1224d980405689a97ffcf7828c56d1517b31d73464356c1f2b7769e \
|
||||
@@ -1166,9 +1170,11 @@ unittest2==1.1.0 \
|
||||
--hash=sha256:13f77d0875db6d9b435e1d4f41e74ad4cc2eb6e1d5c824996092b3430f088bb8 \
|
||||
--hash=sha256:22882a0e418c284e1f718a822b3b022944d53d2d908e1690b319a9d3eb2c0579
|
||||
zope.component==4.2.2 \
|
||||
--hash=sha256:282c112b55dd8e3c869a3571f86767c150ab1284a9ace2bdec226c592acaf81a
|
||||
--hash=sha256:282c112b55dd8e3c869a3571f86767c150ab1284a9ace2bdec226c592acaf81a \
|
||||
--no-binary zope.component
|
||||
zope.event==4.1.0 \
|
||||
--hash=sha256:dc7a59a2fd91730d3793131a5d261b29e93ec4e2a97f1bc487ce8defee2fe786
|
||||
--hash=sha256:dc7a59a2fd91730d3793131a5d261b29e93ec4e2a97f1bc487ce8defee2fe786 \
|
||||
--no-binary zope.event
|
||||
zope.interface==4.1.3 \
|
||||
--hash=sha256:f07b631f7a601cd8cbd3332d54f43142c7088a83299f859356f08d1d4d4259b3 \
|
||||
--hash=sha256:de5cca083b9439d8002fb76bbe6b4998c5a5a721fab25b84298967f002df4c94 \
|
||||
@@ -1187,6 +1193,9 @@ zope.interface==4.1.3 \
|
||||
--hash=sha256:928138365245a0e8869a5999fbcc2a45475a0a6ed52a494d60dbdc540335fedd \
|
||||
--hash=sha256:0d841ba1bb840eea0e6489dc5ecafa6125554971f53b5acb87764441e61bceba \
|
||||
--hash=sha256:b09c8c1d47b3531c400e0195697f1414a63221de6ef478598a4f1460f7d9a392
|
||||
requests-toolbelt==0.8.0 \
|
||||
--hash=sha256:42c9c170abc2cacb78b8ab23ac957945c7716249206f90874651971a4acff237 \
|
||||
--hash=sha256:f6a531936c6fa4c6cfce1b9c10d5c4f498d16528d2a54a22ca00011205a187b5
|
||||
|
||||
# Contains the requirements for the letsencrypt package.
|
||||
#
|
||||
@@ -1199,18 +1208,18 @@ letsencrypt==0.7.0 \
|
||||
--hash=sha256:105a5fb107e45bcd0722eb89696986dcf5f08a86a321d6aef25a0c7c63375ade \
|
||||
--hash=sha256:c36e532c486a7e92155ee09da54b436a3c420813ec1c590b98f635d924720de9
|
||||
|
||||
certbot==0.24.0 \
|
||||
--hash=sha256:a3fc41fde4f0dbb35f7ebec2f9e00339580b3f4298850411eac0719223073b27 \
|
||||
--hash=sha256:a072d4528bb3ac4184f5c961a96931795ddfe4b7cb0f3a98954bdd4cce5f6d70
|
||||
acme==0.24.0 \
|
||||
--hash=sha256:b92b16102051f447abb52917638fbfb34b646ac07267fee85961b360a0149e32 \
|
||||
--hash=sha256:d655e0627c0830114ab3f6732d8bf2f4a2c36f602e0cde10988684e229b501cb
|
||||
certbot-apache==0.24.0 \
|
||||
--hash=sha256:fe54db3e7e09ffe1139041c23ff5123e80aa1526d6fcd40b2a593d005cfcf152 \
|
||||
--hash=sha256:686c6c0af5ae8d06e37cc762de7ffa0dc5c3b1ba06ff7653ef61713fa016f891
|
||||
certbot-nginx==0.24.0 \
|
||||
--hash=sha256:d44c419f72c2cc30de3b138a2cf92e0531696dcb048f287036e229dce2131c00 \
|
||||
--hash=sha256:3283d1db057261f05537fa408baee20e0ab9e81c3d55cfba70afe3805cd6f941
|
||||
certbot==0.25.1 \
|
||||
--hash=sha256:01689015364685fef3f1e1fb7832ba84eb3b0aa85bc5a71c96661f6d4c59981f \
|
||||
--hash=sha256:5c23e5186133bb1afd805be5e0cd2fb7b95862a8b0459c9ecad4ae60f933e54e
|
||||
acme==0.25.1 \
|
||||
--hash=sha256:26e641a01536705fe5f12d856703b8ef06e5a07981a7b6379d2771dcdb69a742 \
|
||||
--hash=sha256:47b5f3f73d69b7b1d13f918aa2cd75a8093069a68becf4af38e428e4613b2734
|
||||
certbot-apache==0.25.1 \
|
||||
--hash=sha256:a28b7c152cc11474bef5b5e7967aaea42b2c0aaf86fd82ee4082713d33cee5a9 \
|
||||
--hash=sha256:ed012465617073a0f1057fe854dc8d1eb6d2dd7ede1fb2eee765129fed2a095a
|
||||
certbot-nginx==0.25.1 \
|
||||
--hash=sha256:83f82c3ba08c0b1d4bf449ac24018e8e7dd34a6248d35466f2de7da1cd312e15 \
|
||||
--hash=sha256:68f98b41c54e0bf4218ef293079597176617bee3837ae3aa6528ce2ff0bf4f9c
|
||||
|
||||
UNLIKELY_EOF
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
-----BEGIN PGP SIGNATURE-----
|
||||
|
||||
iQEzBAABCAAdFiEEos+1H6J1pyhiNOeyTRfJlc2XdfIFAlro/1AACgkQTRfJlc2X
|
||||
dfLm5ggAxCrWU9dmYZKllcFzp7TFOdRap0pmarfL4gwSYj7B/bSceD7ysOyoQ8Ra
|
||||
7UHuZKAQyurZn1seN49d88Kgor9KWZQ1jZiGkfiEpp8qAkdWzFR8UqYa2/CZtk2l
|
||||
bExm8YQDwhuKvCObGLDGi3ydcIQpfg/rsBkSTphKYXN/Zebx9mAelZN4CgGRy03Y
|
||||
3z2UqqnyqFPAg4wUGcNfCgUEbJ5bUPr733vQzjBS2IVUbDbu06/1Y8oYzurezXNS
|
||||
6lEyvTfC5G8RGlSWupNu7yWviD14M4LnAo6WXWEVH+C+ssJaPrZVhZ6KfEt/Erg3
|
||||
k06WZSPDCtOm5EfhDm0Rumqm1owA2g==
|
||||
=Bc4G
|
||||
iQEzBAABCAAdFiEEos+1H6J1pyhiNOeyTRfJlc2XdfIFAlsgc/cACgkQTRfJlc2X
|
||||
dfLjBgf/bHZn/q+Dqn34uBXHymRSce7UxQn17izcKAt7hZBl4j4sebQ9+0jjuNur
|
||||
zrW8b0XJ0PsI10GG9qHR3ajC+04pWfRritnK1g4Ycb/pDcUkWo+8uRwr7skAVcvC
|
||||
oa8ToBS3iUbd3csFl1mu1BGACUHLvVs2cYdDtMuJj8wjsVZ7KnWBGKULAskwmU4Z
|
||||
VVUxeUrG9f+2kT35meEJUk91FS+4tmqNIVsVlBzf0Q0ZU1iQnV56dMwTqFRzdDJ2
|
||||
DBecE0GwuYnKXo2I7kIYaqACQmk9YFh55Sh0K9PbQxyv7YEZXZtkcdqFqyhxy3Nh
|
||||
EJ2kurFaM3/VmLljc/rW8QW8B3QNbw==
|
||||
=pkDz
|
||||
-----END PGP SIGNATURE-----
|
||||
|
||||
@@ -31,7 +31,7 @@ if [ -z "$VENV_PATH" ]; then
|
||||
fi
|
||||
VENV_BIN="$VENV_PATH/bin"
|
||||
BOOTSTRAP_VERSION_PATH="$VENV_PATH/certbot-auto-bootstrap-version.txt"
|
||||
LE_AUTO_VERSION="0.25.0.dev0"
|
||||
LE_AUTO_VERSION="0.26.0.dev0"
|
||||
BASENAME=$(basename $0)
|
||||
USAGE="Usage: $BASENAME [OPTIONS]
|
||||
A self-updating wrapper script for the Certbot ACME client. When run, updates
|
||||
@@ -1060,37 +1060,26 @@ ConfigArgParse==0.12.0 \
|
||||
configobj==5.0.6 \
|
||||
--hash=sha256:a2f5650770e1c87fb335af19a9b7eb73fc05ccf22144eb68db7d00cd2bcb0902 \
|
||||
--no-binary configobj
|
||||
cryptography==2.0.2 \
|
||||
--hash=sha256:187ae17358436d2c760f28c2aeb02fefa3f37647a9c5b6f7f7c3e83cd1c5a972 \
|
||||
--hash=sha256:19e43a13bbf52028dd1e810c803f2ad8880d0692d772f98d42e1eaf34bdee3d6 \
|
||||
--hash=sha256:da9291502cbc87dc0284a20c56876e4d2e68deac61cc43df4aec934e44ca97b1 \
|
||||
--hash=sha256:0954f8813095f581669330e0a2d5e726c33ac7f450c1458fac58bab54595e516 \
|
||||
--hash=sha256:d68b0cc40a8432ed3fc84876c519de704d6001800ec22b136e75ae841910c45b \
|
||||
--hash=sha256:2f8ad9580ab4da645cfea52a91d2da99a49a1e76616d8be68441a986fad652b0 \
|
||||
--hash=sha256:cc00b4511294f5f6b65c4e77a1a9c62f52490a63d2c120f3872176b40a82351e \
|
||||
--hash=sha256:cf896020f6a9f095a547b3d672c8db1ef2ed71fca11250731fa1d4a4cb8b1590 \
|
||||
--hash=sha256:e0fdb8322206fa02aa38f71519ff75dce2eb481b7e1110e2936795cb376bb6ee \
|
||||
--hash=sha256:277538466657ca5d6637f80be100242f9831d75138b788d718edd3aab34621f8 \
|
||||
--hash=sha256:2c77eb0560f54ce654ab82d6b2a64327a71ee969b29022bf9746ca311c9f5069 \
|
||||
--hash=sha256:755a7853b679e79d0a799351c092a9b0271f95ff54c8dd8823d8b527a2926a86 \
|
||||
--hash=sha256:77197a2d525e761cdd4c771180b4bd0d80703654c6385e4311cbbbe2beb56fa1 \
|
||||
--hash=sha256:eb8bb79d0ab00c931c8333b745f06fec481a51c52d70acd4ee95d6093ba5c386 \
|
||||
--hash=sha256:131f61de82ef28f3e20beb4bfc24f9692d28cecfd704e20e6c7f070f7793013a \
|
||||
--hash=sha256:ac35435974b2e27cd4520f29c191d7da36f4189aa3264e52c4c6c6d089ab6142 \
|
||||
--hash=sha256:04b6ea99daa2a8460728794213d76d45ad58ea247dc7e7ff148d7dd726e87863 \
|
||||
--hash=sha256:2b9442f8b4c3d575f6cc3db0e856034e0f5a9d55ecd636f52d8c496795b26952 \
|
||||
--hash=sha256:b3d3b3ecba1fe1bdb6f180770a137f877c8f07571f7b2934bb269475bcf0e5e8 \
|
||||
--hash=sha256:670a58c0d75cb0e78e73dd003bd96d4440bbb1f2bc041dcf7b81767ca4fb0ce9 \
|
||||
--hash=sha256:5af84d23bdb86b5e90aca263df1424b43f1748480bfcde3ac2a3cbe622612468 \
|
||||
--hash=sha256:ba22e8eefabdd7aca37d0c0c00d2274000d2cebb5cce9e5a710cb55bf8797b31 \
|
||||
--hash=sha256:b798b22fa7e92b439547323b8b719d217f1e1b7677585cfeeedf3b55c70bb7fb \
|
||||
--hash=sha256:59cff28af8cce96cb7e94a459726e1d88f6f5fa75097f9dcbebd99118d64ea4c \
|
||||
--hash=sha256:fe859e445abc9ba9e97950ddafb904e23234c4ecb76b0fae6c86e80592ce464a \
|
||||
--hash=sha256:655f3c474067f1e277430f23cc0549f0b1dc99b82aec6e53f80b9b2db7f76f11 \
|
||||
--hash=sha256:0ebc2be053c9a03a2f3e20a466e87bf12a51586b3c79bd2a22171b073a805346 \
|
||||
--hash=sha256:01e6e60654df64cca53733cda39446d67100c819c181d403afb120e0d2a71e1b \
|
||||
--hash=sha256:d46f4e5d455cb5563685c52ef212696f0a6cc1ea627603218eabbd8a095291d8 \
|
||||
--hash=sha256:3780b2663ee7ebb37cb83263326e3cd7f8b2ea439c448539d4b87de12c8d06ab
|
||||
cryptography==2.2.2 \
|
||||
--hash=sha256:3f3b65d5a16e6b52fba63dc860b62ca9832f51f1a2ae5083c78b6840275f12dd \
|
||||
--hash=sha256:5251e7de0de66810833606439ca65c9b9e45da62196b0c88bfadf27740aac09f \
|
||||
--hash=sha256:551a3abfe0c8c6833df4192a63371aa2ff43afd8f570ed345d31f251d78e7e04 \
|
||||
--hash=sha256:5cb990056b7cadcca26813311187ad751ea644712022a3976443691168781b6f \
|
||||
--hash=sha256:60bda7f12ecb828358be53095fc9c6edda7de8f1ef571f96c00b2363643fa3cd \
|
||||
--hash=sha256:64b5c67acc9a7c83fbb4b69166f3105a0ab722d27934fac2cb26456718eec2ba \
|
||||
--hash=sha256:6fef51ec447fe9f8351894024e94736862900d3a9aa2961528e602eb65c92bdb \
|
||||
--hash=sha256:77d0ad229d47a6e0272d00f6bf8ac06ce14715a9fd02c9a97f5a2869aab3ccb2 \
|
||||
--hash=sha256:808fe471b1a6b777f026f7dc7bd9a4959da4bfab64972f2bbe91e22527c1c037 \
|
||||
--hash=sha256:9b62fb4d18529c84b961efd9187fecbb48e89aa1a0f9f4161c61b7fc42a101bd \
|
||||
--hash=sha256:9e5bed45ec6b4f828866ac6a6bedf08388ffcfa68abe9e94b34bb40977aba531 \
|
||||
--hash=sha256:9fc295bf69130a342e7a19a39d7bbeb15c0bcaabc7382ec33ef3b2b7d18d2f63 \
|
||||
--hash=sha256:abd070b5849ed64e6d349199bef955ee0ad99aefbad792f0c587f8effa681a5e \
|
||||
--hash=sha256:ba6a774749b6e510cffc2fb98535f717e0e5fd91c7c99a61d223293df79ab351 \
|
||||
--hash=sha256:c332118647f084c983c6a3e1dba0f3bcb051f69d12baccac68db8d62d177eb8a \
|
||||
--hash=sha256:d6f46e862ee36df81e6342c2177ba84e70f722d9dc9c6c394f9f1f434c4a5563 \
|
||||
--hash=sha256:db6013746f73bf8edd9c3d1d3f94db635b9422f503db3fc5ef105233d4c011ab \
|
||||
--hash=sha256:f57008eaff597c69cf692c3518f6d4800f0309253bb138b526a37fe9ef0c7471 \
|
||||
--hash=sha256:f6c821ac253c19f2ad4c8691633ae1d1a17f120d5b01ea1d256d7b602bc59887
|
||||
enum34==1.1.2 ; python_version < '3.4' \
|
||||
--hash=sha256:2475d7fcddf5951e92ff546972758802de5260bf409319a9f1934e6bbc8b1dc7 \
|
||||
--hash=sha256:35907defb0f992b75ab7788f65fedc1cf20ffa22688e0e6f6f12afc06b3ea501
|
||||
@@ -1208,18 +1197,18 @@ letsencrypt==0.7.0 \
|
||||
--hash=sha256:105a5fb107e45bcd0722eb89696986dcf5f08a86a321d6aef25a0c7c63375ade \
|
||||
--hash=sha256:c36e532c486a7e92155ee09da54b436a3c420813ec1c590b98f635d924720de9
|
||||
|
||||
certbot==0.24.0 \
|
||||
--hash=sha256:a3fc41fde4f0dbb35f7ebec2f9e00339580b3f4298850411eac0719223073b27 \
|
||||
--hash=sha256:a072d4528bb3ac4184f5c961a96931795ddfe4b7cb0f3a98954bdd4cce5f6d70
|
||||
acme==0.24.0 \
|
||||
--hash=sha256:b92b16102051f447abb52917638fbfb34b646ac07267fee85961b360a0149e32 \
|
||||
--hash=sha256:d655e0627c0830114ab3f6732d8bf2f4a2c36f602e0cde10988684e229b501cb
|
||||
certbot-apache==0.24.0 \
|
||||
--hash=sha256:fe54db3e7e09ffe1139041c23ff5123e80aa1526d6fcd40b2a593d005cfcf152 \
|
||||
--hash=sha256:686c6c0af5ae8d06e37cc762de7ffa0dc5c3b1ba06ff7653ef61713fa016f891
|
||||
certbot-nginx==0.24.0 \
|
||||
--hash=sha256:d44c419f72c2cc30de3b138a2cf92e0531696dcb048f287036e229dce2131c00 \
|
||||
--hash=sha256:3283d1db057261f05537fa408baee20e0ab9e81c3d55cfba70afe3805cd6f941
|
||||
certbot==0.25.1 \
|
||||
--hash=sha256:01689015364685fef3f1e1fb7832ba84eb3b0aa85bc5a71c96661f6d4c59981f \
|
||||
--hash=sha256:5c23e5186133bb1afd805be5e0cd2fb7b95862a8b0459c9ecad4ae60f933e54e
|
||||
acme==0.25.1 \
|
||||
--hash=sha256:26e641a01536705fe5f12d856703b8ef06e5a07981a7b6379d2771dcdb69a742 \
|
||||
--hash=sha256:47b5f3f73d69b7b1d13f918aa2cd75a8093069a68becf4af38e428e4613b2734
|
||||
certbot-apache==0.25.1 \
|
||||
--hash=sha256:a28b7c152cc11474bef5b5e7967aaea42b2c0aaf86fd82ee4082713d33cee5a9 \
|
||||
--hash=sha256:ed012465617073a0f1057fe854dc8d1eb6d2dd7ede1fb2eee765129fed2a095a
|
||||
certbot-nginx==0.25.1 \
|
||||
--hash=sha256:83f82c3ba08c0b1d4bf449ac24018e8e7dd34a6248d35466f2de7da1cd312e15 \
|
||||
--hash=sha256:68f98b41c54e0bf4218ef293079597176617bee3837ae3aa6528ce2ff0bf4f9c
|
||||
|
||||
UNLIKELY_EOF
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
Binary file not shown.
@@ -1,12 +1,12 @@
|
||||
certbot==0.24.0 \
|
||||
--hash=sha256:a3fc41fde4f0dbb35f7ebec2f9e00339580b3f4298850411eac0719223073b27 \
|
||||
--hash=sha256:a072d4528bb3ac4184f5c961a96931795ddfe4b7cb0f3a98954bdd4cce5f6d70
|
||||
acme==0.24.0 \
|
||||
--hash=sha256:b92b16102051f447abb52917638fbfb34b646ac07267fee85961b360a0149e32 \
|
||||
--hash=sha256:d655e0627c0830114ab3f6732d8bf2f4a2c36f602e0cde10988684e229b501cb
|
||||
certbot-apache==0.24.0 \
|
||||
--hash=sha256:fe54db3e7e09ffe1139041c23ff5123e80aa1526d6fcd40b2a593d005cfcf152 \
|
||||
--hash=sha256:686c6c0af5ae8d06e37cc762de7ffa0dc5c3b1ba06ff7653ef61713fa016f891
|
||||
certbot-nginx==0.24.0 \
|
||||
--hash=sha256:d44c419f72c2cc30de3b138a2cf92e0531696dcb048f287036e229dce2131c00 \
|
||||
--hash=sha256:3283d1db057261f05537fa408baee20e0ab9e81c3d55cfba70afe3805cd6f941
|
||||
certbot==0.25.1 \
|
||||
--hash=sha256:01689015364685fef3f1e1fb7832ba84eb3b0aa85bc5a71c96661f6d4c59981f \
|
||||
--hash=sha256:5c23e5186133bb1afd805be5e0cd2fb7b95862a8b0459c9ecad4ae60f933e54e
|
||||
acme==0.25.1 \
|
||||
--hash=sha256:26e641a01536705fe5f12d856703b8ef06e5a07981a7b6379d2771dcdb69a742 \
|
||||
--hash=sha256:47b5f3f73d69b7b1d13f918aa2cd75a8093069a68becf4af38e428e4613b2734
|
||||
certbot-apache==0.25.1 \
|
||||
--hash=sha256:a28b7c152cc11474bef5b5e7967aaea42b2c0aaf86fd82ee4082713d33cee5a9 \
|
||||
--hash=sha256:ed012465617073a0f1057fe854dc8d1eb6d2dd7ede1fb2eee765129fed2a095a
|
||||
certbot-nginx==0.25.1 \
|
||||
--hash=sha256:83f82c3ba08c0b1d4bf449ac24018e8e7dd34a6248d35466f2de7da1cd312e15 \
|
||||
--hash=sha256:68f98b41c54e0bf4218ef293079597176617bee3837ae3aa6528ce2ff0bf4f9c
|
||||
|
||||
@@ -64,37 +64,26 @@ ConfigArgParse==0.12.0 \
|
||||
configobj==5.0.6 \
|
||||
--hash=sha256:a2f5650770e1c87fb335af19a9b7eb73fc05ccf22144eb68db7d00cd2bcb0902 \
|
||||
--no-binary configobj
|
||||
cryptography==2.0.2 \
|
||||
--hash=sha256:187ae17358436d2c760f28c2aeb02fefa3f37647a9c5b6f7f7c3e83cd1c5a972 \
|
||||
--hash=sha256:19e43a13bbf52028dd1e810c803f2ad8880d0692d772f98d42e1eaf34bdee3d6 \
|
||||
--hash=sha256:da9291502cbc87dc0284a20c56876e4d2e68deac61cc43df4aec934e44ca97b1 \
|
||||
--hash=sha256:0954f8813095f581669330e0a2d5e726c33ac7f450c1458fac58bab54595e516 \
|
||||
--hash=sha256:d68b0cc40a8432ed3fc84876c519de704d6001800ec22b136e75ae841910c45b \
|
||||
--hash=sha256:2f8ad9580ab4da645cfea52a91d2da99a49a1e76616d8be68441a986fad652b0 \
|
||||
--hash=sha256:cc00b4511294f5f6b65c4e77a1a9c62f52490a63d2c120f3872176b40a82351e \
|
||||
--hash=sha256:cf896020f6a9f095a547b3d672c8db1ef2ed71fca11250731fa1d4a4cb8b1590 \
|
||||
--hash=sha256:e0fdb8322206fa02aa38f71519ff75dce2eb481b7e1110e2936795cb376bb6ee \
|
||||
--hash=sha256:277538466657ca5d6637f80be100242f9831d75138b788d718edd3aab34621f8 \
|
||||
--hash=sha256:2c77eb0560f54ce654ab82d6b2a64327a71ee969b29022bf9746ca311c9f5069 \
|
||||
--hash=sha256:755a7853b679e79d0a799351c092a9b0271f95ff54c8dd8823d8b527a2926a86 \
|
||||
--hash=sha256:77197a2d525e761cdd4c771180b4bd0d80703654c6385e4311cbbbe2beb56fa1 \
|
||||
--hash=sha256:eb8bb79d0ab00c931c8333b745f06fec481a51c52d70acd4ee95d6093ba5c386 \
|
||||
--hash=sha256:131f61de82ef28f3e20beb4bfc24f9692d28cecfd704e20e6c7f070f7793013a \
|
||||
--hash=sha256:ac35435974b2e27cd4520f29c191d7da36f4189aa3264e52c4c6c6d089ab6142 \
|
||||
--hash=sha256:04b6ea99daa2a8460728794213d76d45ad58ea247dc7e7ff148d7dd726e87863 \
|
||||
--hash=sha256:2b9442f8b4c3d575f6cc3db0e856034e0f5a9d55ecd636f52d8c496795b26952 \
|
||||
--hash=sha256:b3d3b3ecba1fe1bdb6f180770a137f877c8f07571f7b2934bb269475bcf0e5e8 \
|
||||
--hash=sha256:670a58c0d75cb0e78e73dd003bd96d4440bbb1f2bc041dcf7b81767ca4fb0ce9 \
|
||||
--hash=sha256:5af84d23bdb86b5e90aca263df1424b43f1748480bfcde3ac2a3cbe622612468 \
|
||||
--hash=sha256:ba22e8eefabdd7aca37d0c0c00d2274000d2cebb5cce9e5a710cb55bf8797b31 \
|
||||
--hash=sha256:b798b22fa7e92b439547323b8b719d217f1e1b7677585cfeeedf3b55c70bb7fb \
|
||||
--hash=sha256:59cff28af8cce96cb7e94a459726e1d88f6f5fa75097f9dcbebd99118d64ea4c \
|
||||
--hash=sha256:fe859e445abc9ba9e97950ddafb904e23234c4ecb76b0fae6c86e80592ce464a \
|
||||
--hash=sha256:655f3c474067f1e277430f23cc0549f0b1dc99b82aec6e53f80b9b2db7f76f11 \
|
||||
--hash=sha256:0ebc2be053c9a03a2f3e20a466e87bf12a51586b3c79bd2a22171b073a805346 \
|
||||
--hash=sha256:01e6e60654df64cca53733cda39446d67100c819c181d403afb120e0d2a71e1b \
|
||||
--hash=sha256:d46f4e5d455cb5563685c52ef212696f0a6cc1ea627603218eabbd8a095291d8 \
|
||||
--hash=sha256:3780b2663ee7ebb37cb83263326e3cd7f8b2ea439c448539d4b87de12c8d06ab
|
||||
cryptography==2.2.2 \
|
||||
--hash=sha256:3f3b65d5a16e6b52fba63dc860b62ca9832f51f1a2ae5083c78b6840275f12dd \
|
||||
--hash=sha256:5251e7de0de66810833606439ca65c9b9e45da62196b0c88bfadf27740aac09f \
|
||||
--hash=sha256:551a3abfe0c8c6833df4192a63371aa2ff43afd8f570ed345d31f251d78e7e04 \
|
||||
--hash=sha256:5cb990056b7cadcca26813311187ad751ea644712022a3976443691168781b6f \
|
||||
--hash=sha256:60bda7f12ecb828358be53095fc9c6edda7de8f1ef571f96c00b2363643fa3cd \
|
||||
--hash=sha256:64b5c67acc9a7c83fbb4b69166f3105a0ab722d27934fac2cb26456718eec2ba \
|
||||
--hash=sha256:6fef51ec447fe9f8351894024e94736862900d3a9aa2961528e602eb65c92bdb \
|
||||
--hash=sha256:77d0ad229d47a6e0272d00f6bf8ac06ce14715a9fd02c9a97f5a2869aab3ccb2 \
|
||||
--hash=sha256:808fe471b1a6b777f026f7dc7bd9a4959da4bfab64972f2bbe91e22527c1c037 \
|
||||
--hash=sha256:9b62fb4d18529c84b961efd9187fecbb48e89aa1a0f9f4161c61b7fc42a101bd \
|
||||
--hash=sha256:9e5bed45ec6b4f828866ac6a6bedf08388ffcfa68abe9e94b34bb40977aba531 \
|
||||
--hash=sha256:9fc295bf69130a342e7a19a39d7bbeb15c0bcaabc7382ec33ef3b2b7d18d2f63 \
|
||||
--hash=sha256:abd070b5849ed64e6d349199bef955ee0ad99aefbad792f0c587f8effa681a5e \
|
||||
--hash=sha256:ba6a774749b6e510cffc2fb98535f717e0e5fd91c7c99a61d223293df79ab351 \
|
||||
--hash=sha256:c332118647f084c983c6a3e1dba0f3bcb051f69d12baccac68db8d62d177eb8a \
|
||||
--hash=sha256:d6f46e862ee36df81e6342c2177ba84e70f722d9dc9c6c394f9f1f434c4a5563 \
|
||||
--hash=sha256:db6013746f73bf8edd9c3d1d3f94db635b9422f503db3fc5ef105233d4c011ab \
|
||||
--hash=sha256:f57008eaff597c69cf692c3518f6d4800f0309253bb138b526a37fe9ef0c7471 \
|
||||
--hash=sha256:f6c821ac253c19f2ad4c8691633ae1d1a17f120d5b01ea1d256d7b602bc59887
|
||||
enum34==1.1.2 ; python_version < '3.4' \
|
||||
--hash=sha256:2475d7fcddf5951e92ff546972758802de5260bf409319a9f1934e6bbc8b1dc7 \
|
||||
--hash=sha256:35907defb0f992b75ab7788f65fedc1cf20ffa22688e0e6f6f12afc06b3ea501
|
||||
|
||||
@@ -1 +1 @@
|
||||
-e acme[dev]
|
||||
acme[dev]==0.25.0
|
||||
|
||||
5
setup.py
5
setup.py
@@ -2,8 +2,7 @@ import codecs
|
||||
import os
|
||||
import re
|
||||
|
||||
from setuptools import setup
|
||||
from setuptools import find_packages
|
||||
from setuptools import find_packages, setup
|
||||
|
||||
# Workaround for http://bugs.python.org/issue8876, see
|
||||
# http://bugs.python.org/issue8876#msg208792
|
||||
@@ -33,7 +32,7 @@ version = meta['version']
|
||||
# specified here to avoid masking the more specific request requirements in
|
||||
# acme. See https://github.com/pypa/pip/issues/988 for more info.
|
||||
install_requires = [
|
||||
'acme>0.24.0',
|
||||
'acme>=0.25.0',
|
||||
# We technically need ConfigArgParse 0.10.0 for Python 2.6 support, but
|
||||
# saying so here causes a runtime error against our temporary fork of 0.9.3
|
||||
# in which we added 2.6 support (see #2243), so we relax the requirement.
|
||||
|
||||
@@ -486,11 +486,11 @@ if [ "${BOULDER_INTEGRATION:-v1}" = "v2" ]; then
|
||||
--manual-cleanup-hook ./tests/manual-dns-cleanup.sh
|
||||
fi
|
||||
|
||||
coverage report --fail-under 65 --include 'certbot/*' --show-missing
|
||||
|
||||
# Most CI systems set this variable to true.
|
||||
# If the tests are running as part of CI, Nginx should be available.
|
||||
if ${CI:-false} || type nginx;
|
||||
then
|
||||
. ./certbot-nginx/tests/boulder-integration.sh
|
||||
fi
|
||||
|
||||
coverage report --fail-under 67 -m
|
||||
|
||||
@@ -87,6 +87,14 @@ if [ "$RELEASE_BRANCH" != "candidate-$version" ] ; then
|
||||
fi
|
||||
git checkout "$RELEASE_BRANCH"
|
||||
|
||||
for pkg_dir in $SUBPKGS_NO_CERTBOT certbot-compatibility-test .
|
||||
do
|
||||
sed -i 's/\.dev0//' "$pkg_dir/setup.py"
|
||||
done
|
||||
# We only add Certbot's setup.py here because the other files are added in the
|
||||
# call to SetVersion below.
|
||||
git add -p setup.py
|
||||
|
||||
SetVersion() {
|
||||
ver="$1"
|
||||
# bumping Certbot's version number is done differently
|
||||
@@ -153,7 +161,8 @@ kill $!
|
||||
cd ~-
|
||||
|
||||
# get a snapshot of the CLI help for the docs
|
||||
certbot --help all > docs/cli-help.txt
|
||||
# We set CERTBOT_DOCS to use dummy values in example user-agent string.
|
||||
CERTBOT_DOCS=1 certbot --help all > docs/cli-help.txt
|
||||
jws --help > acme/docs/jws-help.txt
|
||||
|
||||
cd ..
|
||||
|
||||
22
tox.ini
22
tox.ini
@@ -14,8 +14,7 @@ pip_install = {toxinidir}/tools/pip_install_editable.sh
|
||||
# before the script moves on to the next package. All dependencies are pinned
|
||||
# to a specific version for increased stability for developers.
|
||||
install_and_test = {toxinidir}/tools/install_and_test.sh
|
||||
dns_packages =
|
||||
certbot-dns-cloudflare \
|
||||
python37_compatible_dns_packages =
|
||||
certbot-dns-cloudxns \
|
||||
certbot-dns-digitalocean \
|
||||
certbot-dns-dnsimple \
|
||||
@@ -25,14 +24,22 @@ dns_packages =
|
||||
certbot-dns-nsone \
|
||||
certbot-dns-rfc2136 \
|
||||
certbot-dns-route53
|
||||
all_packages =
|
||||
dns_packages =
|
||||
certbot-dns-cloudflare \
|
||||
{[base]python37_compatible_dns_packages}
|
||||
nondns_packages =
|
||||
acme[dev] \
|
||||
.[dev] \
|
||||
certbot-apache \
|
||||
{[base]dns_packages} \
|
||||
certbot-nginx \
|
||||
certbot-postfix \
|
||||
letshelp-certbot
|
||||
python37_compatible_packages =
|
||||
{[base]nondns_packages} \
|
||||
{[base]python37_compatible_dns_packages}
|
||||
all_packages =
|
||||
{[base]nondns_packages} \
|
||||
{[base]dns_packages}
|
||||
install_packages =
|
||||
{toxinidir}/tools/pip_install_editable.sh {[base]all_packages}
|
||||
source_paths =
|
||||
@@ -62,6 +69,13 @@ commands =
|
||||
setenv =
|
||||
PYTHONHASHSEED = 0
|
||||
|
||||
[testenv:py37]
|
||||
commands =
|
||||
{[base]install_and_test} {[base]python37_compatible_packages}
|
||||
python tests/lock_test.py
|
||||
setenv =
|
||||
{[testenv]setenv}
|
||||
|
||||
[testenv:py27-oldest]
|
||||
commands =
|
||||
{[testenv]commands}
|
||||
|
||||
Reference in New Issue
Block a user