Compare commits
272 Commits
test-every
...
none_strin
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
50cb6975bc | ||
|
|
43f2bfd6f1 | ||
|
|
cdf93de338 | ||
|
|
dd600db436 | ||
|
|
2564566e1c | ||
|
|
08378203df | ||
|
|
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 | ||
|
|
66953435c9 | ||
|
|
dde0bf0821 | ||
|
|
142bc33545 | ||
|
|
a339de80f4 | ||
|
|
d663f7981a | ||
|
|
72637b2cf6 | ||
|
|
d0ea5958f9 | ||
|
|
4805fb4b88 | ||
|
|
11b820c0e4 | ||
|
|
b21b66c0c0 | ||
|
|
218e15c9d4 | ||
|
|
a6c08a2e25 | ||
|
|
68dc678eed | ||
|
|
5f3be9b1cf | ||
|
|
2ae187b1d6 | ||
|
|
b4b5c44750 | ||
|
|
a29a99fb6f | ||
|
|
0f4c5c2305 | ||
|
|
60c6cc5f2a | ||
|
|
00e28592b6 | ||
|
|
b92df1b71c | ||
|
|
baf0d3343a | ||
|
|
cc3896d5d4 | ||
|
|
b94e268f83 | ||
|
|
b9177948d3 | ||
|
|
3b2e9e49be | ||
|
|
0efc02d6ee | ||
|
|
967a1830e6 | ||
|
|
90ffe2aac0 | ||
|
|
b342f40c2b | ||
|
|
83e37acc8b | ||
|
|
d1f3a2deef | ||
|
|
c9813a44d7 | ||
|
|
b98f541b91 | ||
|
|
50a1f6340f | ||
|
|
8c4ff5cb63 | ||
|
|
290f5b8ce7 | ||
|
|
48c5731a6b | ||
|
|
749f758adb | ||
|
|
2e8a8dfed5 | ||
|
|
25d1f6ec75 | ||
|
|
4c4b63437f | ||
|
|
02c7eca6da | ||
|
|
b72dfc0c08 | ||
|
|
7334fc3066 | ||
|
|
4e5740615c | ||
|
|
5a1d031f07 | ||
|
|
4715b2b12c | ||
|
|
5beaae3b65 | ||
|
|
dfd1cceb9b | ||
|
|
192f0f60da | ||
|
|
b395b72d1b | ||
|
|
a2dbf2fe4c | ||
|
|
86fe5ad362 | ||
|
|
89ae874f89 | ||
|
|
2a217189a6 | ||
|
|
481fb8413b | ||
|
|
d97a15861b | ||
|
|
a15fe57225 | ||
|
|
6c5a8423b8 | ||
|
|
a66500ea38 | ||
|
|
49cdfcec06 | ||
|
|
b50a71ff4e | ||
|
|
b37be61807 | ||
|
|
4a3fd19c93 | ||
|
|
66ba0b5276 | ||
|
|
61c2209110 | ||
|
|
694746409f | ||
|
|
1c258c0a2c | ||
|
|
6c4b3c08a7 | ||
|
|
c2a8ce59ae | ||
|
|
5bf4ad1f52 | ||
|
|
ae08dc6bea | ||
|
|
f89051cc2a | ||
|
|
74b22a596e | ||
|
|
e2d95b3719 | ||
|
|
dca274085d | ||
|
|
619e273ae5 | ||
|
|
baa563f359 | ||
|
|
64f2ddfa80 | ||
|
|
a5f23b5314 | ||
|
|
af38c30c9c | ||
|
|
887871833d | ||
|
|
5d07b70269 | ||
|
|
c43602c908 | ||
|
|
4d24eb83a8 | ||
|
|
7edceec8ac | ||
|
|
c6baa82ee4 | ||
|
|
e75bafa439 | ||
|
|
cc83e9ba52 | ||
|
|
5d355044c4 | ||
|
|
1f95ac9640 | ||
|
|
5928fae89e | ||
|
|
0bf2537a55 | ||
|
|
fd1cef3fa0 | ||
|
|
fee9c86233 | ||
|
|
5cc317408c | ||
|
|
2900d5122c | ||
|
|
c7a8d1cb7a | ||
|
|
0ba508ee2d | ||
|
|
1d37e94e17 | ||
|
|
843e156a51 | ||
|
|
ce88098ba6 | ||
|
|
1cde7f9b54 | ||
|
|
e42a222c5d | ||
|
|
87022782fb | ||
|
|
a58c652443 | ||
|
|
0ce1684ba6 | ||
|
|
af1e94be5a | ||
|
|
e88eac65da | ||
|
|
874c59012a | ||
|
|
b2977ad6a9 | ||
|
|
a435036a1e | ||
|
|
3a8e1d7a70 | ||
|
|
1210c04f14 | ||
|
|
6e2b6a0817 | ||
|
|
9a122626b9 | ||
|
|
d0629e62ea | ||
|
|
b8ce13f96a | ||
|
|
6db2858825 | ||
|
|
4d14423a21 | ||
|
|
959e943de8 | ||
|
|
9e42f6ed08 | ||
|
|
bd20b50879 | ||
|
|
28bb0eb6ac | ||
|
|
074fef773b | ||
|
|
3aeb62cf7e | ||
|
|
8f5b8558d2 | ||
|
|
47a5b7e3ba | ||
|
|
cdc8b94823 | ||
|
|
fedf970284 | ||
|
|
965027ce52 | ||
|
|
1cde095dc2 | ||
|
|
39a01190d5 | ||
|
|
9539f21390 | ||
|
|
9abef4c0bd | ||
|
|
146fce3878 | ||
|
|
904dc11b03 | ||
|
|
7c6c3efb0f | ||
|
|
c87b5d6a78 | ||
|
|
9a71b18b85 | ||
|
|
6da5de6b19 | ||
|
|
147f58bdbc | ||
|
|
fe17c873c0 | ||
|
|
613a8f5e88 | ||
|
|
f04e8259a9 | ||
|
|
828c00b758 | ||
|
|
1d47acddfd | ||
|
|
726afb8b95 | ||
|
|
a1d016d031 | ||
|
|
91f6be6b3b | ||
|
|
97cce82e5a | ||
|
|
622fc72dc1 | ||
|
|
42c63cb6dd | ||
|
|
31e320d0a7 | ||
|
|
7f9dadd681 | ||
|
|
ad40618897 | ||
|
|
e0edc1b7ec | ||
|
|
78a55c3823 | ||
|
|
9cafcf1caf | ||
|
|
8c6d28ce95 | ||
|
|
30ba7e9305 | ||
|
|
a6700e3172 | ||
|
|
0bd8134e5f | ||
|
|
ff5810d78f | ||
|
|
21ff3acf93 | ||
|
|
cebc6f9a20 | ||
|
|
749c4e39e0 | ||
|
|
6a1aa8e6b6 | ||
|
|
bdd4d01dc7 | ||
|
|
6a40e1964b | ||
|
|
82ef8b185a | ||
|
|
dd4f9d35ae | ||
|
|
127d49e837 | ||
|
|
a85fad98c0 | ||
|
|
1c3c69aaad | ||
|
|
3de8c2a651 | ||
|
|
803c39e585 | ||
|
|
51980e212f | ||
|
|
3df343495e | ||
|
|
45aeb5b003 | ||
|
|
67ee3b0488 | ||
|
|
2eba47a716 | ||
|
|
51f90ffafb | ||
|
|
8d6b6c358a | ||
|
|
9da4f93ae5 | ||
|
|
3cf61a54b7 | ||
|
|
43d457aa77 | ||
|
|
499f6c2fad | ||
|
|
9cd71642fb | ||
|
|
7a7329fa19 | ||
|
|
9ce047980a | ||
|
|
02abaf57bd | ||
|
|
3d7b53daf1 | ||
|
|
e99abfacfd | ||
|
|
a2ee328bc0 | ||
|
|
34cba3accf | ||
|
|
3712a45399 | ||
|
|
182e9b29e4 | ||
|
|
2540f1f1e8 | ||
|
|
eea1b0d8c5 | ||
|
|
6e1bcfdb2a | ||
|
|
d7e4d93190 | ||
|
|
0c4e332811 | ||
|
|
46ce09d36d | ||
|
|
269f15f9ee | ||
|
|
21e841fd13 | ||
|
|
a03db04ff4 | ||
|
|
d0bcc13059 | ||
|
|
17425e7337 | ||
|
|
7bdb63376c | ||
|
|
bdbc46fc84 | ||
|
|
0d43d2988a | ||
|
|
79924108c7 | ||
|
|
c033905b16 | ||
|
|
839c523048 | ||
|
|
7c81f23a07 | ||
|
|
e534a43d1a | ||
|
|
fcd1a98201 | ||
|
|
dc606eac7d | ||
|
|
372c96d9fd | ||
|
|
5a9f90dc30 | ||
|
|
ce0a6a1814 | ||
|
|
fa5acdf674 | ||
|
|
31db3b7034 | ||
|
|
30938260d4 | ||
|
|
7bd06a4d35 | ||
|
|
ace9d2383d | ||
|
|
ed0c024209 | ||
|
|
714cb17dcb | ||
|
|
6fb51d5422 | ||
|
|
4b5b9f164f | ||
|
|
3d9d5607bd | ||
|
|
aa417eec15 | ||
|
|
844ec79f01 | ||
|
|
f0b9ef2716 | ||
|
|
8857302347 |
@@ -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:
|
||||
|
||||
@@ -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,152 +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)
|
||||
|
||||
|
||||
# Instead of using a ASN.1 encoding library just append the OCTET STRING tag (0x04)
|
||||
# and the length of the SHA256 hash (0x20) since both of these should never change
|
||||
der_value = b"DER:0420" + 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()
|
||||
# Add the ASN.1 tag/length prefix to the hash before comparison
|
||||
return data == b'\x04\x20' + 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.
|
||||
|
||||
This class simply allows parsing the TLS-ALPN-01 challenge returned from
|
||||
the CA. Full TLS-ALPN-01 support is not currently provided.
|
||||
|
||||
"""
|
||||
typ = "tls-alpn-01"
|
||||
|
||||
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`
|
||||
|
||||
"""
|
||||
return self.response(account_key).gen_cert(key=kwargs.get('cert_key'))
|
||||
"""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:
|
||||
self.assertEqual(jose.ComparableX509(cert),
|
||||
test_util.load_comparable_cert(
|
||||
'rsa2048_cert.pem'))
|
||||
break
|
||||
@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()
|
||||
|
||||
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()
|
||||
# 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'))
|
||||
|
||||
|
||||
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-----
|
||||
@@ -68,6 +68,7 @@ setup(
|
||||
'Programming Language :: Python :: 3.4',
|
||||
'Programming Language :: Python :: 3.5',
|
||||
'Programming Language :: Python :: 3.6',
|
||||
'Programming Language :: Python :: 3.7',
|
||||
'Topic :: Internet :: WWW/HTTP',
|
||||
'Topic :: Security',
|
||||
],
|
||||
|
||||
@@ -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,8 +161,11 @@ 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._prepared = False
|
||||
self.parser = None
|
||||
self.version = version
|
||||
self.vhosts = None
|
||||
@@ -246,6 +250,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
||||
logger.debug("Encountered error:", exc_info=True)
|
||||
raise errors.PluginError(
|
||||
"Unable to lock %s", self.conf("server-root"))
|
||||
self._prepared = True
|
||||
|
||||
def _check_aug_version(self):
|
||||
""" Checks that we have recent enough version of libaugeas.
|
||||
@@ -1472,6 +1477,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 +1597,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 +2296,180 @@ 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):
|
||||
# If installer hasn't been prepared yet, do it now
|
||||
if not self._prepared:
|
||||
self.prepare()
|
||||
# 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
|
||||
|
||||
|
||||
184
certbot-apache/certbot_apache/tests/autohsts_test.py
Normal file
184
certbot-apache/certbot_apache/tests/autohsts_test.py
Normal file
@@ -0,0 +1,184 @@
|
||||
# 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")
|
||||
@mock.patch("certbot_apache.configurator.ApacheConfigurator.prepare")
|
||||
def test_autohsts_increase(self, mock_prepare, _mock_restart):
|
||||
self.config._prepared = False
|
||||
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)
|
||||
self.assertTrue(mock_prepare.called)
|
||||
|
||||
@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]
|
||||
|
||||
@@ -7,8 +7,8 @@ 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',
|
||||
@@ -43,6 +43,7 @@ setup(
|
||||
'Programming Language :: Python :: 3.4',
|
||||
'Programming Language :: Python :: 3.5',
|
||||
'Programming Language :: Python :: 3.6',
|
||||
'Programming Language :: Python :: 3.7',
|
||||
'Topic :: Internet :: WWW/HTTP',
|
||||
'Topic :: Security',
|
||||
'Topic :: System :: Installation/Setup',
|
||||
|
||||
@@ -46,6 +46,7 @@ setup(
|
||||
'Programming Language :: Python :: 3.4',
|
||||
'Programming Language :: Python :: 3.5',
|
||||
'Programming Language :: Python :: 3.6',
|
||||
'Programming Language :: Python :: 3.7',
|
||||
'Topic :: Internet :: WWW/HTTP',
|
||||
'Topic :: Security',
|
||||
],
|
||||
|
||||
@@ -42,6 +42,7 @@ setup(
|
||||
'Programming Language :: Python :: 3.4',
|
||||
'Programming Language :: Python :: 3.5',
|
||||
'Programming Language :: Python :: 3.6',
|
||||
'Programming Language :: Python :: 3.7',
|
||||
'Topic :: Internet :: WWW/HTTP',
|
||||
'Topic :: Security',
|
||||
'Topic :: System :: Installation/Setup',
|
||||
|
||||
@@ -42,6 +42,7 @@ setup(
|
||||
'Programming Language :: Python :: 3.4',
|
||||
'Programming Language :: Python :: 3.5',
|
||||
'Programming Language :: Python :: 3.6',
|
||||
'Programming Language :: Python :: 3.7',
|
||||
'Topic :: Internet :: WWW/HTTP',
|
||||
'Topic :: Security',
|
||||
'Topic :: System :: Installation/Setup',
|
||||
|
||||
@@ -43,6 +43,7 @@ setup(
|
||||
'Programming Language :: Python :: 3.4',
|
||||
'Programming Language :: Python :: 3.5',
|
||||
'Programming Language :: Python :: 3.6',
|
||||
'Programming Language :: Python :: 3.7',
|
||||
'Topic :: Internet :: WWW/HTTP',
|
||||
'Topic :: Security',
|
||||
'Topic :: System :: Installation/Setup',
|
||||
|
||||
@@ -42,6 +42,7 @@ setup(
|
||||
'Programming Language :: Python :: 3.4',
|
||||
'Programming Language :: Python :: 3.5',
|
||||
'Programming Language :: Python :: 3.6',
|
||||
'Programming Language :: Python :: 3.7',
|
||||
'Topic :: Internet :: WWW/HTTP',
|
||||
'Topic :: Security',
|
||||
'Topic :: System :: Installation/Setup',
|
||||
|
||||
@@ -42,6 +42,7 @@ setup(
|
||||
'Programming Language :: Python :: 3.4',
|
||||
'Programming Language :: Python :: 3.5',
|
||||
'Programming Language :: Python :: 3.6',
|
||||
'Programming Language :: Python :: 3.7',
|
||||
'Topic :: Internet :: WWW/HTTP',
|
||||
'Topic :: Security',
|
||||
'Topic :: System :: Installation/Setup',
|
||||
|
||||
@@ -47,6 +47,7 @@ setup(
|
||||
'Programming Language :: Python :: 3.4',
|
||||
'Programming Language :: Python :: 3.5',
|
||||
'Programming Language :: Python :: 3.6',
|
||||
'Programming Language :: Python :: 3.7',
|
||||
'Topic :: Internet :: WWW/HTTP',
|
||||
'Topic :: Security',
|
||||
'Topic :: System :: Installation/Setup',
|
||||
|
||||
@@ -42,6 +42,7 @@ setup(
|
||||
'Programming Language :: Python :: 3.4',
|
||||
'Programming Language :: Python :: 3.5',
|
||||
'Programming Language :: Python :: 3.6',
|
||||
'Programming Language :: Python :: 3.7',
|
||||
'Topic :: Internet :: WWW/HTTP',
|
||||
'Topic :: Security',
|
||||
'Topic :: System :: Installation/Setup',
|
||||
|
||||
@@ -42,6 +42,7 @@ setup(
|
||||
'Programming Language :: Python :: 3.4',
|
||||
'Programming Language :: Python :: 3.5',
|
||||
'Programming Language :: Python :: 3.6',
|
||||
'Programming Language :: Python :: 3.7',
|
||||
'Topic :: Internet :: WWW/HTTP',
|
||||
'Topic :: Security',
|
||||
'Topic :: System :: Installation/Setup',
|
||||
|
||||
@@ -42,6 +42,7 @@ setup(
|
||||
'Programming Language :: Python :: 3.4',
|
||||
'Programming Language :: Python :: 3.5',
|
||||
'Programming Language :: Python :: 3.6',
|
||||
'Programming Language :: Python :: 3.7',
|
||||
'Topic :: Internet :: WWW/HTTP',
|
||||
'Topic :: Security',
|
||||
'Topic :: System :: Installation/Setup',
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
-e acme[dev]
|
||||
acme[dev]==0.25.0
|
||||
certbot[dev]==0.21.1
|
||||
|
||||
@@ -6,7 +6,7 @@ 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',
|
||||
@@ -36,6 +36,7 @@ setup(
|
||||
'Programming Language :: Python :: 3.4',
|
||||
'Programming Language :: Python :: 3.5',
|
||||
'Programming Language :: Python :: 3.6',
|
||||
'Programming Language :: Python :: 3.7',
|
||||
'Topic :: Internet :: WWW/HTTP',
|
||||
'Topic :: Security',
|
||||
'Topic :: System :: Installation/Setup',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -8,7 +8,7 @@ version = '0.26.0.dev0'
|
||||
# acme/certbot version.
|
||||
install_requires = [
|
||||
'acme>=0.25.0',
|
||||
'certbot>0.21.1',
|
||||
'certbot>=0.22.0',
|
||||
'mock',
|
||||
'PyOpenSSL',
|
||||
'pyparsing>=1.5.5', # Python3 support; perhaps unnecessary?
|
||||
@@ -43,6 +43,7 @@ setup(
|
||||
'Programming Language :: Python :: 3.4',
|
||||
'Programming Language :: Python :: 3.5',
|
||||
'Programming Language :: Python :: 3.6',
|
||||
'Programming Language :: Python :: 3.7',
|
||||
'Topic :: Internet :: WWW/HTTP',
|
||||
'Topic :: Security',
|
||||
'Topic :: System :: Installation/Setup',
|
||||
|
||||
@@ -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
|
||||
|
||||
190
certbot-postfix/LICENSE.txt
Normal file
190
certbot-postfix/LICENSE.txt
Normal file
@@ -0,0 +1,190 @@
|
||||
Copyright 2017 Electronic Frontier Foundation and others
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
4
certbot-postfix/MANIFEST.in
Normal file
4
certbot-postfix/MANIFEST.in
Normal file
@@ -0,0 +1,4 @@
|
||||
include LICENSE.txt
|
||||
include README.rst
|
||||
recursive-include certbot_postfix/testdata *
|
||||
recursive-include certbot_postfix/docs *
|
||||
23
certbot-postfix/README.rst
Normal file
23
certbot-postfix/README.rst
Normal file
@@ -0,0 +1,23 @@
|
||||
==========================
|
||||
Postfix plugin for Certbot
|
||||
==========================
|
||||
|
||||
Note: this MTA installer is in **developer beta**-- we appreciate any testing, feedback, or
|
||||
feature requests for this plugin.
|
||||
|
||||
To install this plugin, in the root of this repo, run::
|
||||
|
||||
./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>
|
||||
3
certbot-postfix/certbot_postfix/__init__.py
Normal file
3
certbot-postfix/certbot_postfix/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
"""Certbot Postfix plugin."""
|
||||
|
||||
from certbot_postfix.installer import Installer
|
||||
63
certbot-postfix/certbot_postfix/constants.py
Normal file
63
certbot-postfix/certbot_postfix/constants.py
Normal file
@@ -0,0 +1,63 @@
|
||||
"""Postfix plugin constants."""
|
||||
|
||||
# pylint: disable=unused-import, no-name-in-module
|
||||
from acme.magic_typing import Dict, Tuple, Union
|
||||
# pylint: enable=unused-import, no-name-in-module
|
||||
|
||||
MINIMUM_VERSION = (2, 11,)
|
||||
|
||||
# If the value of a default VAR is a tuple, then the values which
|
||||
# come LATER in the tuple are more strict/more secure.
|
||||
# Certbot will default to the first value in the tuple, but will
|
||||
# not override "more secure" settings.
|
||||
|
||||
ACCEPTABLE_SERVER_SECURITY_LEVELS = ("may", "encrypt")
|
||||
ACCEPTABLE_CLIENT_SECURITY_LEVELS = ("may", "encrypt",
|
||||
"dane", "dane-only",
|
||||
"fingerprint",
|
||||
"verify", "secure")
|
||||
ACCEPTABLE_CIPHER_LEVELS = ("medium", "high")
|
||||
|
||||
# Exporting certain ciphers to prevent logjam: https://weakdh.org/sysadmin.html
|
||||
EXCLUDE_CIPHERS = ("aNULL, eNULL, EXPORT, DES, RC4, MD5, PSK, aECDH, "
|
||||
"EDH-DSS-DES-CBC3-SHA, EDH-RSA-DES-CBC3-SHA, KRB5-DES, CBC3-SHA",)
|
||||
|
||||
|
||||
TLS_VERSIONS = ("SSLv2", "SSLv3", "TLSv1", "TLSv1.1", "TLSv1.2")
|
||||
# Should NOT use SSLv2/3.
|
||||
ACCEPTABLE_TLS_VERSIONS = ("TLSv1", "TLSv1.1", "TLSv1.2")
|
||||
|
||||
# Variables associated with enabling opportunistic TLS.
|
||||
TLS_SERVER_VARS = {
|
||||
"smtpd_tls_security_level": ACCEPTABLE_SERVER_SECURITY_LEVELS,
|
||||
} # type:Dict[str, Tuple[str, ...]]
|
||||
TLS_CLIENT_VARS = {
|
||||
"smtp_tls_security_level": ACCEPTABLE_CLIENT_SECURITY_LEVELS,
|
||||
} # type:Dict[str, Tuple[str, ...]]
|
||||
# Default variables for a secure MTA server [receiver].
|
||||
DEFAULT_SERVER_VARS = {
|
||||
"smtpd_tls_auth_only": ("yes",),
|
||||
"smtpd_tls_mandatory_protocols": ("!SSLv2, !SSLv3",),
|
||||
"smtpd_tls_protocols": ("!SSLv2, !SSLv3",),
|
||||
"smtpd_tls_ciphers": ACCEPTABLE_CIPHER_LEVELS,
|
||||
"smtpd_tls_mandatory_ciphers": ACCEPTABLE_CIPHER_LEVELS,
|
||||
"smtpd_tls_exclude_ciphers": EXCLUDE_CIPHERS,
|
||||
"smtpd_tls_eecdh_grade": ("strong",),
|
||||
} # type:Dict[str, Tuple[str, ...]]
|
||||
|
||||
# Default variables for a secure MTA client [sender].
|
||||
DEFAULT_CLIENT_VARS = {
|
||||
"smtp_tls_ciphers": ACCEPTABLE_CIPHER_LEVELS,
|
||||
"smtp_tls_exclude_ciphers": EXCLUDE_CIPHERS,
|
||||
"smtp_tls_mandatory_ciphers": ACCEPTABLE_CIPHER_LEVELS,
|
||||
} # type:Dict[str, Tuple[str, ...]]
|
||||
|
||||
CLI_DEFAULTS = dict(
|
||||
config_dir="/etc/postfix",
|
||||
ctl="postfix",
|
||||
config_utility="postconf",
|
||||
tls_only=False,
|
||||
ignore_master_overrides=False,
|
||||
server_only=False,
|
||||
)
|
||||
"""CLI defaults."""
|
||||
288
certbot-postfix/certbot_postfix/installer.py
Normal file
288
certbot-postfix/certbot_postfix/installer.py
Normal file
@@ -0,0 +1,288 @@
|
||||
"""certbot installer plugin for postfix."""
|
||||
import logging
|
||||
import os
|
||||
|
||||
import zope.interface
|
||||
import zope.component
|
||||
import six
|
||||
|
||||
from certbot import errors
|
||||
from certbot import interfaces
|
||||
from certbot import util as certbot_util
|
||||
from certbot.plugins import common as plugins_common
|
||||
|
||||
from certbot_postfix import constants
|
||||
from certbot_postfix import postconf
|
||||
from certbot_postfix import util
|
||||
|
||||
# pylint: disable=unused-import, no-name-in-module
|
||||
from acme.magic_typing import Callable, Dict, List
|
||||
# pylint: enable=unused-import, no-name-in-module
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@zope.interface.implementer(interfaces.IInstaller)
|
||||
@zope.interface.provider(interfaces.IPluginFactory)
|
||||
class Installer(plugins_common.Installer):
|
||||
"""Certbot installer plugin for Postfix.
|
||||
|
||||
:ivar str config_dir: Postfix configuration directory to modify
|
||||
:ivar list save_notes: documentation for proposed changes. This is
|
||||
cleared and stored in Certbot checkpoints when save() is called
|
||||
|
||||
:ivar postconf: Wrapper for Postfix configuration command-line tool.
|
||||
:type postconf: :class: `certbot_postfix.postconf.ConfigMain`
|
||||
:ivar postfix: Wrapper for Postfix command-line tool.
|
||||
:type postfix: :class: `certbot_postfix.util.PostfixUtil`
|
||||
"""
|
||||
|
||||
description = "Configure TLS with the Postfix MTA"
|
||||
|
||||
@classmethod
|
||||
def add_parser_arguments(cls, add):
|
||||
add("ctl", default=constants.CLI_DEFAULTS["ctl"],
|
||||
help="Path to the 'postfix' control program.")
|
||||
# This directory points to Postfix's configuration directory.
|
||||
add("config-dir", default=constants.CLI_DEFAULTS["config_dir"],
|
||||
help="Path to the directory containing the "
|
||||
"Postfix main.cf file to modify instead of using the "
|
||||
"default configuration paths.")
|
||||
add("config-utility", default=constants.CLI_DEFAULTS["config_utility"],
|
||||
help="Path to the 'postconf' executable.")
|
||||
add("tls-only", action="store_true", default=constants.CLI_DEFAULTS["tls_only"],
|
||||
help="Only set params to enable opportunistic TLS and install certificates.")
|
||||
add("server-only", action="store_true", default=constants.CLI_DEFAULTS["server_only"],
|
||||
help="Only set server params (prefixed with smtpd*)")
|
||||
add("ignore-master-overrides", action="store_true",
|
||||
default=constants.CLI_DEFAULTS["ignore_master_overrides"],
|
||||
help="Ignore errors reporting overridden TLS parameters in master.cf.")
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(Installer, self).__init__(*args, **kwargs)
|
||||
# Wrapper around postconf commands
|
||||
self.postfix = None
|
||||
self.postconf = None
|
||||
|
||||
# Files to save
|
||||
self.save_notes = [] # type: List[str]
|
||||
|
||||
self._enhance_func = {} # type: Dict[str, Callable[[str, str], None]]
|
||||
# Since we only need to enable TLS once for all domains,
|
||||
# keep track of whether this enhancement was already called.
|
||||
self._tls_enabled = False
|
||||
|
||||
def prepare(self):
|
||||
"""Prepare the installer.
|
||||
|
||||
:raises errors.PluginError: when an unexpected error occurs
|
||||
:raises errors.MisconfigurationError: when the config is invalid
|
||||
:raises errors.NoInstallationError: when can't find installation
|
||||
:raises errors.NotSupportedError: when version is not supported
|
||||
"""
|
||||
# Verify postfix and postconf are installed
|
||||
for param in ("ctl", "config_utility",):
|
||||
util.verify_exe_exists(self.conf(param),
|
||||
"Cannot find executable '{0}'. You can provide the "
|
||||
"path to this command with --{1}".format(
|
||||
self.conf(param),
|
||||
self.option_name(param)))
|
||||
|
||||
# Set up CLI tools
|
||||
self.postfix = util.PostfixUtil(self.conf('config-dir'))
|
||||
self.postconf = postconf.ConfigMain(self.conf('config-utility'),
|
||||
self.conf('ignore-master-overrides'),
|
||||
self.conf('config-dir'))
|
||||
|
||||
# Ensure current configuration is valid.
|
||||
self.config_test()
|
||||
|
||||
# Check Postfix version
|
||||
self._check_version()
|
||||
self._lock_config_dir()
|
||||
self.install_ssl_dhparams()
|
||||
|
||||
def config_test(self):
|
||||
"""Test to see that the current Postfix configuration is valid.
|
||||
|
||||
:raises errors.MisconfigurationError: If the configuration is invalid.
|
||||
"""
|
||||
self.postfix.test()
|
||||
|
||||
def _check_version(self):
|
||||
"""Verifies that the installed Postfix version is supported.
|
||||
|
||||
:raises errors.NotSupportedError: if the version is unsupported
|
||||
"""
|
||||
if self._get_version() < constants.MINIMUM_VERSION:
|
||||
version_string = '.'.join([str(n) for n in constants.MINIMUM_VERSION])
|
||||
raise errors.NotSupportedError('Postfix version must be at least %s' % version_string)
|
||||
|
||||
def _lock_config_dir(self):
|
||||
"""Stop two Postfix plugins from modifying the config at once.
|
||||
|
||||
:raises .PluginError: if unable to acquire the lock
|
||||
"""
|
||||
try:
|
||||
certbot_util.lock_dir_until_exit(self.conf('config-dir'))
|
||||
except (OSError, errors.LockError):
|
||||
logger.debug("Encountered error:", exc_info=True)
|
||||
raise errors.PluginError(
|
||||
"Unable to lock %s" % self.conf('config-dir'))
|
||||
|
||||
def more_info(self):
|
||||
"""Human-readable string to help the user. Describes steps taken and any relevant
|
||||
info to help the user decide which plugin to use.
|
||||
|
||||
:rtype: str
|
||||
"""
|
||||
return (
|
||||
"Configures Postfix to try to authenticate mail servers, use "
|
||||
"installed certificates and disable weak ciphers and protocols.{0}"
|
||||
"Server root: {root}{0}"
|
||||
"Version: {version}".format(
|
||||
os.linesep,
|
||||
root=self.conf('config-dir'),
|
||||
version='.'.join([str(i) for i in self._get_version()]))
|
||||
)
|
||||
|
||||
def _get_version(self):
|
||||
"""Return the version of Postfix, as a tuple. (e.g. '2.11.3' is (2, 11, 3))
|
||||
|
||||
:returns: version
|
||||
:rtype: tuple
|
||||
|
||||
:raises errors.PluginError: Unable to find Postfix version.
|
||||
"""
|
||||
mail_version = self.postconf.get_default("mail_version")
|
||||
return tuple(int(i) for i in mail_version.split('.'))
|
||||
|
||||
def get_all_names(self):
|
||||
"""Returns all names that may be authenticated.
|
||||
|
||||
:rtype: `set` of `str`
|
||||
|
||||
"""
|
||||
return certbot_util.get_filtered_names(self.postconf.get(var)
|
||||
for var in ('mydomain', 'myhostname', 'myorigin',))
|
||||
|
||||
def _set_vars(self, var_dict):
|
||||
"""Sets all parameters in var_dict to config file. If current value is already set
|
||||
as more secure (acceptable), then don't set/overwrite it.
|
||||
"""
|
||||
for param, acceptable in six.iteritems(var_dict):
|
||||
if not util.is_acceptable_value(param, self.postconf.get(param), acceptable):
|
||||
self.postconf.set(param, acceptable[0], acceptable)
|
||||
|
||||
def _confirm_changes(self):
|
||||
"""Confirming outstanding updates for configuration parameters.
|
||||
|
||||
:raises errors.PluginError: when user rejects the configuration changes.
|
||||
"""
|
||||
updates = self.postconf.get_changes()
|
||||
output_string = "Postfix TLS configuration parameters to update in main.cf:\n"
|
||||
for name, value in six.iteritems(updates):
|
||||
output_string += "{0} = {1}\n".format(name, value)
|
||||
output_string += "Is this okay?\n"
|
||||
if not zope.component.getUtility(interfaces.IDisplay).yesno(output_string,
|
||||
force_interactive=True, default=True):
|
||||
raise errors.PluginError(
|
||||
"Manually rejected configuration changes.\n"
|
||||
"Try using --tls-only or --server-only to change a particular"
|
||||
"subset of configuration parameters.")
|
||||
|
||||
def deploy_cert(self, domain, cert_path,
|
||||
key_path, chain_path, fullchain_path):
|
||||
"""Configure the Postfix SMTP server to use the given TLS cert.
|
||||
|
||||
:param str domain: domain to deploy certificate file
|
||||
:param str cert_path: absolute path to the certificate file
|
||||
:param str key_path: absolute path to the private key file
|
||||
:param str chain_path: absolute path to the certificate chain file
|
||||
:param str fullchain_path: absolute path to the certificate fullchain
|
||||
file (cert plus chain)
|
||||
|
||||
:raises .PluginError: when cert cannot be deployed
|
||||
|
||||
"""
|
||||
# pylint: disable=unused-argument
|
||||
if self._tls_enabled:
|
||||
return
|
||||
self._tls_enabled = True
|
||||
self.save_notes.append("Configuring TLS for {0}".format(domain))
|
||||
self.postconf.set("smtpd_tls_cert_file", cert_path)
|
||||
self.postconf.set("smtpd_tls_key_file", key_path)
|
||||
self._set_vars(constants.TLS_SERVER_VARS)
|
||||
if not self.conf('server_only'):
|
||||
self._set_vars(constants.TLS_CLIENT_VARS)
|
||||
if not self.conf('tls_only'):
|
||||
self._set_vars(constants.DEFAULT_SERVER_VARS)
|
||||
if not self.conf('server_only'):
|
||||
self._set_vars(constants.DEFAULT_CLIENT_VARS)
|
||||
# Despite the name, this option also supports 2048-bit DH params.
|
||||
# http://www.postfix.org/FORWARD_SECRECY_README.html#server_fs
|
||||
self.postconf.set("smtpd_tls_dh1024_param_file", self.ssl_dhparams)
|
||||
self._confirm_changes()
|
||||
|
||||
def enhance(self, domain, enhancement, options=None):
|
||||
"""Raises an exception since this installer doesn't support any enhancements.
|
||||
"""
|
||||
# pylint: disable=unused-argument
|
||||
raise errors.PluginError(
|
||||
"Unsupported enhancement: {0}".format(enhancement))
|
||||
|
||||
def supported_enhancements(self):
|
||||
"""Returns a list of supported enhancements.
|
||||
|
||||
:rtype: list
|
||||
|
||||
"""
|
||||
return []
|
||||
|
||||
def save(self, title=None, temporary=False):
|
||||
"""Creates backups and writes changes to configuration files.
|
||||
|
||||
:param str title: The title of the save. If a title is given, the
|
||||
configuration will be saved as a new checkpoint and put in a
|
||||
timestamped directory. `title` has no effect if temporary is true.
|
||||
|
||||
:param bool temporary: Indicates whether the changes made will
|
||||
be quickly reversed in the future (challenges)
|
||||
|
||||
:raises errors.PluginError: when save is unsuccessful
|
||||
"""
|
||||
save_files = set((os.path.join(self.conf('config-dir'), "main.cf"),))
|
||||
self.add_to_checkpoint(save_files,
|
||||
"\n".join(self.save_notes), temporary)
|
||||
self.postconf.flush()
|
||||
|
||||
del self.save_notes[:]
|
||||
|
||||
if title and not temporary:
|
||||
self.finalize_checkpoint(title)
|
||||
|
||||
def recovery_routine(self):
|
||||
super(Installer, self).recovery_routine()
|
||||
self.postconf = postconf.ConfigMain(self.conf('config-utility'),
|
||||
self.conf('ignore-master-overrides'),
|
||||
self.conf('config-dir'))
|
||||
|
||||
def rollback_checkpoints(self, rollback=1):
|
||||
"""Rollback saved checkpoints.
|
||||
|
||||
:param int rollback: Number of checkpoints to revert
|
||||
|
||||
:raises .errors.PluginError: If there is a problem with the input or
|
||||
the function is unable to correctly revert the configuration
|
||||
"""
|
||||
super(Installer, self).rollback_checkpoints(rollback)
|
||||
self.postconf = postconf.ConfigMain(self.conf('config-utility'),
|
||||
self.conf('ignore-master-overrides'),
|
||||
self.conf('config-dir'))
|
||||
|
||||
def restart(self):
|
||||
"""Restart or refresh the server content.
|
||||
|
||||
:raises .PluginError: when server cannot be restarted
|
||||
"""
|
||||
self.postfix.restart()
|
||||
|
||||
152
certbot-postfix/certbot_postfix/postconf.py
Normal file
152
certbot-postfix/certbot_postfix/postconf.py
Normal file
@@ -0,0 +1,152 @@
|
||||
"""Classes that wrap the postconf command line utility.
|
||||
"""
|
||||
import six
|
||||
from certbot import errors
|
||||
from certbot_postfix import util
|
||||
|
||||
# pylint: disable=unused-import, no-name-in-module
|
||||
from acme.magic_typing import Dict, List, Tuple
|
||||
# pylint: enable=unused-import, no-name-in-module
|
||||
|
||||
class ConfigMain(util.PostfixUtilBase):
|
||||
"""A parser for Postfix's main.cf file."""
|
||||
|
||||
def __init__(self, executable, ignore_master_overrides=False, config_dir=None):
|
||||
super(ConfigMain, self).__init__(executable, config_dir)
|
||||
# Whether to ignore overrides from master.
|
||||
self._ignore_master_overrides = ignore_master_overrides
|
||||
# List of all current Postfix parameters, from `postconf` command.
|
||||
self._db = {} # type: Dict[str, str]
|
||||
# List of current master.cf overrides from Postfix config. Dictionary
|
||||
# of parameter name => list of tuples (service name, paramter value)
|
||||
# Note: We should never modify master without explicit permission.
|
||||
self._master_db = {} # type: Dict[str, List[Tuple[str, str]]]
|
||||
# List of all changes requested to the Postfix parameters as they are now
|
||||
# in _db. These changes are flushed to `postconf` on `flush`.
|
||||
self._updated = {} # type: Dict[str, str]
|
||||
self._read_from_conf()
|
||||
|
||||
def _read_from_conf(self):
|
||||
"""Reads initial parameter state from `main.cf` into this object.
|
||||
"""
|
||||
out = self._get_output()
|
||||
for name, value in _parse_main_output(out):
|
||||
self._db[name] = value
|
||||
out = self._get_output_master()
|
||||
for name, value in _parse_main_output(out):
|
||||
service, param_name = name.rsplit("/", 1)
|
||||
if param_name not in self._master_db:
|
||||
self._master_db[param_name] = []
|
||||
self._master_db[param_name].append((service, value))
|
||||
|
||||
def _get_output_master(self):
|
||||
"""Retrieves output for `master.cf` parameters."""
|
||||
return self._get_output('-P')
|
||||
|
||||
def get_default(self, name):
|
||||
"""Retrieves default value of parameter `name` from postfix parameters.
|
||||
|
||||
:param str name: The name of the parameter to fetch.
|
||||
:returns: The default value of parameter `name`.
|
||||
:rtype: str
|
||||
"""
|
||||
out = self._get_output(['-d', name])
|
||||
_, value = next(_parse_main_output(out), (None, None))
|
||||
return value
|
||||
|
||||
def get(self, name):
|
||||
"""Retrieves working value of parameter `name` from postfix parameters.
|
||||
|
||||
:param str name: The name of the parameter to fetch.
|
||||
:returns: The value of parameter `name`.
|
||||
:rtype: str
|
||||
"""
|
||||
if name in self._updated:
|
||||
return self._updated[name]
|
||||
return self._db[name]
|
||||
|
||||
def get_master_overrides(self, name):
|
||||
"""Retrieves list of overrides for parameter `name` in postfix's Master config
|
||||
file.
|
||||
|
||||
:returns: List of tuples (service, value), meaning that parameter `name`
|
||||
is overridden as `value` for `service`.
|
||||
:rtype: `list` of `tuple` of `str`
|
||||
"""
|
||||
if name in self._master_db:
|
||||
return self._master_db[name]
|
||||
return None
|
||||
|
||||
def set(self, name, value, acceptable_overrides=None):
|
||||
"""Sets parameter `name` to `value`. If `name` is overridden by a particular service in
|
||||
`master.cf`, reports any of these parameter conflicts as long as
|
||||
`ignore_master_overrides` was not set.
|
||||
|
||||
.. note:: that this function does not flush these parameter values to main.cf;
|
||||
To do that, use `flush`.
|
||||
|
||||
:param str name: The name of the parameter to set.
|
||||
:param str value: The value of the parameter.
|
||||
:param tuple acceptable_overrides: If the master configuration file overrides `value`
|
||||
with a value in acceptable_overrides.
|
||||
"""
|
||||
if name not in self._db:
|
||||
raise KeyError("Parameter name %s is not a valid Postfix parameter name.", name)
|
||||
# Check to see if this parameter is overridden by master.
|
||||
overrides = self.get_master_overrides(name)
|
||||
if not self._ignore_master_overrides and overrides is not None:
|
||||
util.report_master_overrides(name, overrides, acceptable_overrides)
|
||||
if value != self._db[name]:
|
||||
# _db contains the "original" state of parameters. We only care about
|
||||
# writes if they cause a delta from the original state.
|
||||
self._updated[name] = value
|
||||
elif name in self._updated:
|
||||
# If this write reverts a previously updated parameter back to the
|
||||
# original DB's state, we don't have to keep track of it in _updated.
|
||||
del self._updated[name]
|
||||
|
||||
def flush(self):
|
||||
"""Flushes all parameter changes made using `self.set`, to `main.cf`
|
||||
|
||||
:raises error.PluginError: When flush to main.cf fails for some reason.
|
||||
"""
|
||||
if len(self._updated) == 0:
|
||||
return
|
||||
args = ['-e']
|
||||
for name, value in six.iteritems(self._updated):
|
||||
args.append('{0}={1}'.format(name, value))
|
||||
try:
|
||||
self._get_output(args)
|
||||
except IOError as e:
|
||||
raise errors.PluginError("Unable to save to Postfix config: %v", e)
|
||||
for name, value in six.iteritems(self._updated):
|
||||
self._db[name] = value
|
||||
self._updated = {}
|
||||
|
||||
def get_changes(self):
|
||||
""" Return queued changes to main.cf.
|
||||
|
||||
:rtype: dict[str, str]
|
||||
"""
|
||||
return self._updated
|
||||
|
||||
def _parse_main_output(output):
|
||||
"""Parses the raw output from Postconf about main.cf.
|
||||
|
||||
Expects the output to look like:
|
||||
|
||||
.. code-block:: none
|
||||
|
||||
name1 = value1
|
||||
name2 = value2
|
||||
|
||||
:param str output: data postconf wrote to stdout about main.cf
|
||||
|
||||
:returns: generator providing key-value pairs from main.cf
|
||||
:rtype: Iterator[tuple(str, str)]
|
||||
"""
|
||||
for line in output.splitlines():
|
||||
name, _, value = line.partition(" =")
|
||||
yield name, value.strip()
|
||||
|
||||
|
||||
1
certbot-postfix/certbot_postfix/tests/__init__.py
Normal file
1
certbot-postfix/certbot_postfix/tests/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
""" Certbot Postfix Tests """
|
||||
314
certbot-postfix/certbot_postfix/tests/installer_test.py
Normal file
314
certbot-postfix/certbot_postfix/tests/installer_test.py
Normal file
@@ -0,0 +1,314 @@
|
||||
"""Tests for certbot_postfix.installer."""
|
||||
from contextlib import contextmanager
|
||||
import copy
|
||||
import functools
|
||||
import os
|
||||
import pkg_resources
|
||||
import six
|
||||
import unittest
|
||||
|
||||
import mock
|
||||
|
||||
from certbot import errors
|
||||
from certbot.tests import util as certbot_test_util
|
||||
|
||||
# pylint: disable=unused-import, no-name-in-module
|
||||
from acme.magic_typing import Dict, Tuple, Union
|
||||
# pylint: enable=unused-import, no-name-in-module
|
||||
|
||||
DEFAULT_MAIN_CF = {
|
||||
"smtpd_tls_cert_file": "",
|
||||
"smtpd_tls_key_file": "",
|
||||
"smtpd_tls_dh1024_param_file": "",
|
||||
"smtpd_tls_security_level": "none",
|
||||
"smtpd_tls_auth_only": "",
|
||||
"smtpd_tls_mandatory_protocols": "",
|
||||
"smtpd_tls_protocols": "",
|
||||
"smtpd_tls_ciphers": "",
|
||||
"smtpd_tls_exclude_ciphers": "",
|
||||
"smtpd_tls_mandatory_ciphers": "",
|
||||
"smtpd_tls_eecdh_grade": "medium",
|
||||
"smtp_tls_security_level": "",
|
||||
"smtp_tls_ciphers": "",
|
||||
"smtp_tls_exclude_ciphers": "",
|
||||
"smtp_tls_mandatory_ciphers": "",
|
||||
"mail_version": "3.2.3"
|
||||
}
|
||||
|
||||
def _main_cf_with(obj):
|
||||
main_cf = copy.copy(DEFAULT_MAIN_CF)
|
||||
main_cf.update(obj)
|
||||
return main_cf
|
||||
|
||||
class InstallerTest(certbot_test_util.ConfigTestCase):
|
||||
# pylint: disable=too-many-public-methods
|
||||
|
||||
def setUp(self):
|
||||
super(InstallerTest, self).setUp()
|
||||
_config_file = pkg_resources.resource_filename("certbot_postfix.tests",
|
||||
os.path.join("testdata", "config.json"))
|
||||
self.config.postfix_ctl = "postfix"
|
||||
self.config.postfix_config_dir = self.tempdir
|
||||
self.config.postfix_config_utility = "postconf"
|
||||
self.config.postfix_tls_only = False
|
||||
self.config.postfix_server_only = False
|
||||
self.config.config_dir = self.tempdir
|
||||
|
||||
@mock.patch("certbot_postfix.installer.util.is_acceptable_value")
|
||||
def test_set_vars(self, mock_is_acceptable_value):
|
||||
mock_is_acceptable_value.return_value = True
|
||||
with create_installer(self.config) as installer:
|
||||
installer.prepare()
|
||||
mock_is_acceptable_value.return_value = False
|
||||
|
||||
@mock.patch("certbot_postfix.installer.util.is_acceptable_value")
|
||||
def test_acceptable_value(self, mock_is_acceptable_value):
|
||||
mock_is_acceptable_value.return_value = True
|
||||
with create_installer(self.config) as installer:
|
||||
installer.prepare()
|
||||
mock_is_acceptable_value.return_value = False
|
||||
|
||||
@certbot_test_util.patch_get_utility()
|
||||
def test_confirm_changes_no_raises_error(self, mock_util):
|
||||
mock_util().yesno.return_value = False
|
||||
with create_installer(self.config) as installer:
|
||||
installer.prepare()
|
||||
self.assertRaises(errors.PluginError, installer.deploy_cert,
|
||||
"example.com", "cert_path", "key_path",
|
||||
"chain_path", "fullchain_path")
|
||||
|
||||
@certbot_test_util.patch_get_utility()
|
||||
def test_save(self, mock_util):
|
||||
mock_util().yesno.return_value = True
|
||||
with create_installer(self.config) as installer:
|
||||
installer.prepare()
|
||||
installer.postconf.flush = mock.Mock()
|
||||
installer.reverter = mock.Mock()
|
||||
installer.deploy_cert("example.com", "cert_path", "key_path",
|
||||
"chain_path", "fullchain_path")
|
||||
installer.save()
|
||||
self.assertEqual(installer.save_notes, [])
|
||||
self.assertEqual(installer.postconf.flush.call_count, 1)
|
||||
self.assertEqual(installer.reverter.add_to_checkpoint.call_count, 1)
|
||||
|
||||
@certbot_test_util.patch_get_utility()
|
||||
def test_save_with_title(self, mock_util):
|
||||
mock_util().yesno.return_value = True
|
||||
with create_installer(self.config) as installer:
|
||||
installer.prepare()
|
||||
installer.postconf.flush = mock.Mock()
|
||||
installer.reverter = mock.Mock()
|
||||
installer.deploy_cert("example.com", "cert_path", "key_path",
|
||||
"chain_path", "fullchain_path")
|
||||
installer.save(title="new_file!")
|
||||
self.assertEqual(installer.reverter.finalize_checkpoint.call_count, 1)
|
||||
|
||||
@certbot_test_util.patch_get_utility()
|
||||
def test_rollback_checkpoints_resets_postconf(self, mock_util):
|
||||
mock_util().yesno.return_value = True
|
||||
with create_installer(self.config) as installer:
|
||||
installer.prepare()
|
||||
installer.deploy_cert("example.com", "cert_path", "key_path",
|
||||
"chain_path", "fullchain_path")
|
||||
installer.rollback_checkpoints()
|
||||
self.assertEqual(installer.postconf.get_changes(), {})
|
||||
|
||||
@certbot_test_util.patch_get_utility()
|
||||
def test_recovery_routine_resets_postconf(self, mock_util):
|
||||
mock_util().yesno.return_value = True
|
||||
with create_installer(self.config) as installer:
|
||||
installer.prepare()
|
||||
installer.deploy_cert("example.com", "cert_path", "key_path",
|
||||
"chain_path", "fullchain_path")
|
||||
installer.recovery_routine()
|
||||
self.assertEqual(installer.postconf.get_changes(), {})
|
||||
|
||||
def test_restart(self):
|
||||
with create_installer(self.config) as installer:
|
||||
installer.prepare()
|
||||
installer.restart()
|
||||
self.assertEqual(installer.postfix.restart.call_count, 1)
|
||||
|
||||
def test_add_parser_arguments(self):
|
||||
options = set(("ctl", "config-dir", "config-utility",
|
||||
"tls-only", "server-only", "ignore-master-overrides"))
|
||||
mock_add = mock.MagicMock()
|
||||
|
||||
from certbot_postfix import installer
|
||||
installer.Installer.add_parser_arguments(mock_add)
|
||||
|
||||
for call in mock_add.call_args_list:
|
||||
self.assertTrue(call[0][0] in options)
|
||||
|
||||
def test_no_postconf_prepare(self):
|
||||
with create_installer(self.config) as installer:
|
||||
installer_path = "certbot_postfix.installer"
|
||||
exe_exists_path = installer_path + ".certbot_util.exe_exists"
|
||||
path_surgery_path = "certbot_postfix.util.plugins_util.path_surgery"
|
||||
with mock.patch(path_surgery_path, return_value=False):
|
||||
with mock.patch(exe_exists_path, return_value=False):
|
||||
self.assertRaises(errors.NoInstallationError,
|
||||
installer.prepare)
|
||||
|
||||
def test_old_version(self):
|
||||
with create_installer(self.config, main_cf=_main_cf_with({"mail_version": "0.0.1"}))\
|
||||
as installer:
|
||||
self.assertRaises(errors.NotSupportedError, installer.prepare)
|
||||
|
||||
def test_lock_error(self):
|
||||
with create_installer(self.config) as installer:
|
||||
assert_raises = functools.partial(self.assertRaises,
|
||||
errors.PluginError,
|
||||
installer.prepare)
|
||||
certbot_test_util.lock_and_call(assert_raises, self.tempdir)
|
||||
|
||||
|
||||
@mock.patch('certbot.util.lock_dir_until_exit')
|
||||
def test_dir_locked(self, lock_dir):
|
||||
with create_installer(self.config) as installer:
|
||||
lock_dir.side_effect = errors.LockError
|
||||
self.assertRaises(errors.PluginError, installer.prepare)
|
||||
|
||||
def test_more_info(self):
|
||||
with create_installer(self.config) as installer:
|
||||
installer.prepare()
|
||||
output = installer.more_info()
|
||||
self.assertTrue("Postfix" in output)
|
||||
self.assertTrue(self.tempdir in output)
|
||||
self.assertTrue(DEFAULT_MAIN_CF["mail_version"] in output)
|
||||
|
||||
def test_get_all_names(self):
|
||||
config = {"mydomain": "example.org",
|
||||
"myhostname": "mail.example.org",
|
||||
"myorigin": "example.org"}
|
||||
with create_installer(self.config, main_cf=_main_cf_with(config)) as installer:
|
||||
installer.prepare()
|
||||
result = installer.get_all_names()
|
||||
self.assertEqual(result, set(config.values()))
|
||||
|
||||
@certbot_test_util.patch_get_utility()
|
||||
def test_deploy(self, mock_util):
|
||||
mock_util().yesno.return_value = True
|
||||
from certbot_postfix import constants
|
||||
with create_installer(self.config) as installer:
|
||||
installer.prepare()
|
||||
|
||||
# pylint: disable=protected-access
|
||||
installer.deploy_cert("example.com", "cert_path", "key_path",
|
||||
"chain_path", "fullchain_path")
|
||||
changes = installer.postconf.get_changes()
|
||||
expected = {} # type: Dict[str, Tuple[str, ...]]
|
||||
expected.update(constants.TLS_SERVER_VARS)
|
||||
expected.update(constants.DEFAULT_SERVER_VARS)
|
||||
expected.update(constants.DEFAULT_CLIENT_VARS)
|
||||
self.assertEqual(changes["smtpd_tls_key_file"], "key_path")
|
||||
self.assertEqual(changes["smtpd_tls_cert_file"], "cert_path")
|
||||
for name, value in six.iteritems(expected):
|
||||
self.assertEqual(changes[name], value[0])
|
||||
|
||||
@certbot_test_util.patch_get_utility()
|
||||
def test_tls_only(self, mock_util):
|
||||
mock_util().yesno.return_value = True
|
||||
with create_installer(self.config) as installer:
|
||||
installer.prepare()
|
||||
installer.conf = lambda x: x == "tls_only"
|
||||
installer.postconf.set = mock.Mock()
|
||||
installer.deploy_cert("example.com", "cert_path", "key_path",
|
||||
"chain_path", "fullchain_path")
|
||||
self.assertEqual(installer.postconf.set.call_count, 4)
|
||||
|
||||
@certbot_test_util.patch_get_utility()
|
||||
def test_server_only(self, mock_util):
|
||||
mock_util().yesno.return_value = True
|
||||
with create_installer(self.config) as installer:
|
||||
installer.prepare()
|
||||
installer.conf = lambda x: x == "server_only"
|
||||
installer.postconf.set = mock.Mock()
|
||||
installer.deploy_cert("example.com", "cert_path", "key_path",
|
||||
"chain_path", "fullchain_path")
|
||||
self.assertEqual(installer.postconf.set.call_count, 11)
|
||||
|
||||
@certbot_test_util.patch_get_utility()
|
||||
def test_tls_and_server_only(self, mock_util):
|
||||
mock_util().yesno.return_value = True
|
||||
with create_installer(self.config) as installer:
|
||||
installer.prepare()
|
||||
installer.conf = lambda x: True
|
||||
installer.postconf.set = mock.Mock()
|
||||
installer.deploy_cert("example.com", "cert_path", "key_path",
|
||||
"chain_path", "fullchain_path")
|
||||
self.assertEqual(installer.postconf.set.call_count, 3)
|
||||
|
||||
@certbot_test_util.patch_get_utility()
|
||||
def test_deploy_twice(self, mock_util):
|
||||
# Deploying twice on the same installer shouldn't do anything!
|
||||
mock_util().yesno.return_value = True
|
||||
with create_installer(self.config) as installer:
|
||||
installer.prepare()
|
||||
from certbot_postfix.postconf import ConfigMain
|
||||
with mock.patch.object(ConfigMain, "set", wraps=installer.postconf.set) as fake_set:
|
||||
installer.deploy_cert("example.com", "cert_path", "key_path",
|
||||
"chain_path", "fullchain_path")
|
||||
self.assertEqual(fake_set.call_count, 15)
|
||||
fake_set.reset_mock()
|
||||
installer.deploy_cert("example.com", "cert_path", "key_path",
|
||||
"chain_path", "fullchain_path")
|
||||
fake_set.assert_not_called()
|
||||
|
||||
@certbot_test_util.patch_get_utility()
|
||||
def test_deploy_already_secure(self, mock_util):
|
||||
# Should not overwrite "more-secure" parameters
|
||||
mock_util().yesno.return_value = True
|
||||
more_secure = {
|
||||
"smtpd_tls_security_level": "encrypt",
|
||||
"smtpd_tls_protocols": "!SSLv3, !SSLv2, !TLSv1",
|
||||
"smtpd_tls_eecdh_grade": "strong"
|
||||
}
|
||||
with create_installer(self.config,\
|
||||
main_cf=_main_cf_with(more_secure)) as installer:
|
||||
installer.prepare()
|
||||
installer.deploy_cert("example.com", "cert_path", "key_path",
|
||||
"chain_path", "fullchain_path")
|
||||
for param in more_secure.keys():
|
||||
self.assertFalse(param in installer.postconf.get_changes())
|
||||
|
||||
def test_enhance(self):
|
||||
with create_installer(self.config) as installer:
|
||||
installer.prepare()
|
||||
self.assertRaises(errors.PluginError,
|
||||
installer.enhance,
|
||||
"example.org", "redirect")
|
||||
|
||||
def test_supported_enhancements(self):
|
||||
with create_installer(self.config) as installer:
|
||||
installer.prepare()
|
||||
self.assertEqual(installer.supported_enhancements(), [])
|
||||
|
||||
@contextmanager
|
||||
def create_installer(config, main_cf=DEFAULT_MAIN_CF):
|
||||
# pylint: disable=dangerous-default-value
|
||||
"""Creates a Postfix installer with calls to `postconf` and `postfix` mocked out.
|
||||
|
||||
In particular, creates a ConfigMain object that does regular things, but seeds it
|
||||
with values from `main_cf` and `master_cf` dicts.
|
||||
"""
|
||||
from certbot_postfix.postconf import ConfigMain
|
||||
from certbot_postfix import installer
|
||||
def _mock_init_postconf(postconf, executable, ignore_master_overrides=False, config_dir=None):
|
||||
# pylint: disable=protected-access,unused-argument
|
||||
postconf._ignore_master_overrides = ignore_master_overrides
|
||||
postconf._db = main_cf
|
||||
postconf._master_db = {}
|
||||
postconf._updated = {}
|
||||
# override get_default to get from main
|
||||
postconf.get_default = lambda name: main_cf[name]
|
||||
with mock.patch.object(ConfigMain, "__init__", _mock_init_postconf):
|
||||
exe_exists_path = "certbot_postfix.installer.certbot_util.exe_exists"
|
||||
with mock.patch(exe_exists_path, return_value=True):
|
||||
with mock.patch("certbot_postfix.installer.util.PostfixUtil",
|
||||
return_value=mock.Mock()):
|
||||
yield installer.Installer(config, "postfix")
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main() # pragma: no cover
|
||||
|
||||
107
certbot-postfix/certbot_postfix/tests/postconf_test.py
Normal file
107
certbot-postfix/certbot_postfix/tests/postconf_test.py
Normal file
@@ -0,0 +1,107 @@
|
||||
"""Tests for certbot_postfix.postconf."""
|
||||
|
||||
import mock
|
||||
import unittest
|
||||
|
||||
from certbot import errors
|
||||
|
||||
class PostConfTest(unittest.TestCase):
|
||||
"""Tests for certbot_postfix.util.PostConf."""
|
||||
def setUp(self):
|
||||
from certbot_postfix.postconf import ConfigMain
|
||||
super(PostConfTest, self).setUp()
|
||||
with mock.patch('certbot_postfix.util.PostfixUtilBase._get_output') as mock_call:
|
||||
with mock.patch('certbot_postfix.postconf.ConfigMain._get_output_master') as \
|
||||
mock_master_call:
|
||||
with mock.patch('certbot_postfix.postconf.util.verify_exe_exists') as verify_exe:
|
||||
verify_exe.return_value = True
|
||||
mock_call.return_value = ('default_parameter = value\n'
|
||||
'extra_param =\n'
|
||||
'overridden_by_master = default\n')
|
||||
mock_master_call.return_value = (
|
||||
'service/type/overridden_by_master = master_value\n'
|
||||
'service2/type/overridden_by_master = master_value2\n'
|
||||
)
|
||||
self.config = ConfigMain('postconf', False)
|
||||
|
||||
@mock.patch('certbot_postfix.util.PostfixUtilBase._get_output')
|
||||
@mock.patch('certbot_postfix.postconf.util.verify_exe_exists')
|
||||
def test_get_output_master(self, mock_verify_exe, mock_get_output):
|
||||
from certbot_postfix.postconf import ConfigMain
|
||||
mock_verify_exe.return_value = True
|
||||
ConfigMain('postconf', lambda x, y, z: None)
|
||||
mock_get_output.assert_called_with('-P')
|
||||
|
||||
@mock.patch('certbot_postfix.util.PostfixUtilBase._get_output')
|
||||
def test_read_default(self, mock_get_output):
|
||||
mock_get_output.return_value = 'param = default_value'
|
||||
self.assertEqual(self.config.get_default('param'), 'default_value')
|
||||
|
||||
@mock.patch('certbot_postfix.util.PostfixUtilBase._call')
|
||||
def test_set(self, mock_call):
|
||||
self.config.set('extra_param', 'other_value')
|
||||
self.assertEqual(self.config.get('extra_param'), 'other_value')
|
||||
self.config.flush()
|
||||
mock_call.assert_called_with(['-e', 'extra_param=other_value'])
|
||||
|
||||
def test_set_bad_param_name(self):
|
||||
self.assertRaises(KeyError, self.config.set, 'nonexistent_param', 'some_value')
|
||||
|
||||
@mock.patch('certbot_postfix.util.PostfixUtilBase._call')
|
||||
def test_write_revert(self, mock_call):
|
||||
self.config.set('default_parameter', 'fake_news')
|
||||
# revert config set
|
||||
self.config.set('default_parameter', 'value')
|
||||
self.config.flush()
|
||||
mock_call.assert_not_called()
|
||||
|
||||
@mock.patch('certbot_postfix.util.PostfixUtilBase._call')
|
||||
def test_write_default(self, mock_call):
|
||||
self.config.set('default_parameter', 'value')
|
||||
self.config.flush()
|
||||
mock_call.assert_not_called()
|
||||
|
||||
def test_master_overrides(self):
|
||||
self.assertEqual(self.config.get_master_overrides('overridden_by_master'),
|
||||
[('service/type', 'master_value'),
|
||||
('service2/type', 'master_value2')])
|
||||
|
||||
def test_set_check_override(self):
|
||||
self.assertRaises(errors.PluginError, self.config.set,
|
||||
'overridden_by_master', 'new_value')
|
||||
|
||||
def test_ignore_check_override(self):
|
||||
# pylint: disable=protected-access
|
||||
self.config._ignore_master_overrides = True
|
||||
self.config.set('overridden_by_master', 'new_value')
|
||||
|
||||
def test_check_acceptable_overrides(self):
|
||||
self.config.set('overridden_by_master', 'new_value',
|
||||
('master_value', 'master_value2'))
|
||||
|
||||
@mock.patch('certbot_postfix.util.PostfixUtilBase._get_output')
|
||||
def test_flush(self, mock_out):
|
||||
self.config.set('default_parameter', 'new_value')
|
||||
self.config.set('extra_param', 'another_value')
|
||||
self.config.flush()
|
||||
arguments = mock_out.call_args_list[-1][0][0]
|
||||
self.assertEquals('-e', arguments[0])
|
||||
self.assertTrue('default_parameter=new_value' in arguments)
|
||||
self.assertTrue('extra_param=another_value' in arguments)
|
||||
|
||||
@mock.patch('certbot_postfix.util.PostfixUtilBase._get_output')
|
||||
def test_flush_updates_object(self, mock_out):
|
||||
self.config.set('default_parameter', 'new_value')
|
||||
self.config.flush()
|
||||
mock_out.reset_mock()
|
||||
self.config.set('default_parameter', 'new_value')
|
||||
mock_out.assert_not_called()
|
||||
|
||||
@mock.patch('certbot_postfix.util.PostfixUtilBase._get_output')
|
||||
def test_flush_throws_error_on_fail(self, mock_out):
|
||||
mock_out.side_effect = [IOError("oh no!")]
|
||||
self.config.set('default_parameter', 'new_value')
|
||||
self.assertRaises(errors.PluginError, self.config.flush)
|
||||
|
||||
if __name__ == '__main__': # pragma: no cover
|
||||
unittest.main()
|
||||
205
certbot-postfix/certbot_postfix/tests/util_test.py
Normal file
205
certbot-postfix/certbot_postfix/tests/util_test.py
Normal file
@@ -0,0 +1,205 @@
|
||||
"""Tests for certbot_postfix.util."""
|
||||
|
||||
import subprocess
|
||||
import unittest
|
||||
|
||||
import mock
|
||||
|
||||
from certbot import errors
|
||||
|
||||
|
||||
class PostfixUtilBaseTest(unittest.TestCase):
|
||||
"""Tests for certbot_postfix.util.PostfixUtilBase."""
|
||||
|
||||
@classmethod
|
||||
def _create_object(cls, *args, **kwargs):
|
||||
from certbot_postfix.util import PostfixUtilBase
|
||||
return PostfixUtilBase(*args, **kwargs)
|
||||
|
||||
@mock.patch('certbot_postfix.util.verify_exe_exists')
|
||||
def test_no_exe(self, mock_verify):
|
||||
expected_error = errors.NoInstallationError
|
||||
mock_verify.side_effect = expected_error
|
||||
self.assertRaises(expected_error, self._create_object, 'nonexistent')
|
||||
|
||||
def test_object_creation(self):
|
||||
with mock.patch('certbot_postfix.util.verify_exe_exists'):
|
||||
self._create_object('existent')
|
||||
|
||||
@mock.patch('certbot_postfix.util.check_all_output')
|
||||
def test_call_extends_args(self, mock_output):
|
||||
# pylint: disable=protected-access
|
||||
with mock.patch('certbot_postfix.util.verify_exe_exists'):
|
||||
mock_output.return_value = 'expected'
|
||||
postfix = self._create_object('executable')
|
||||
postfix._call(['many', 'extra', 'args'])
|
||||
mock_output.assert_called_with(['executable', 'many', 'extra', 'args'])
|
||||
postfix._call()
|
||||
mock_output.assert_called_with(['executable'])
|
||||
|
||||
def test_create_with_config(self):
|
||||
# pylint: disable=protected-access
|
||||
with mock.patch('certbot_postfix.util.verify_exe_exists'):
|
||||
postfix = self._create_object('exec', 'config_dir')
|
||||
self.assertEqual(postfix._base_command, ['exec', '-c', 'config_dir'])
|
||||
|
||||
class PostfixUtilTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
# pylint: disable=protected-access
|
||||
from certbot_postfix.util import PostfixUtil
|
||||
with mock.patch('certbot_postfix.util.verify_exe_exists'):
|
||||
self.postfix = PostfixUtil()
|
||||
self.postfix._call = mock.Mock()
|
||||
self.mock_call = self.postfix._call
|
||||
|
||||
def test_test(self):
|
||||
self.postfix.test()
|
||||
self.mock_call.assert_called_with(['check'])
|
||||
|
||||
def test_test_raises_error_when_check_fails(self):
|
||||
self.mock_call.side_effect = [subprocess.CalledProcessError(1, "")]
|
||||
self.assertRaises(errors.MisconfigurationError, self.postfix.test)
|
||||
self.mock_call.assert_called_with(['check'])
|
||||
|
||||
def test_restart_while_running(self):
|
||||
self.mock_call.side_effect = [subprocess.CalledProcessError(1, ""), None]
|
||||
self.postfix.restart()
|
||||
self.mock_call.assert_called_with(['start'])
|
||||
|
||||
def test_restart_while_not_running(self):
|
||||
self.postfix.restart()
|
||||
self.mock_call.assert_called_with(['reload'])
|
||||
|
||||
def test_restart_raises_error_when_reload_fails(self):
|
||||
self.mock_call.side_effect = [None, subprocess.CalledProcessError(1, "")]
|
||||
self.assertRaises(errors.PluginError, self.postfix.restart)
|
||||
self.mock_call.assert_called_with(['reload'])
|
||||
|
||||
def test_restart_raises_error_when_start_fails(self):
|
||||
self.mock_call.side_effect = [
|
||||
subprocess.CalledProcessError(1, ""),
|
||||
subprocess.CalledProcessError(1, "")]
|
||||
self.assertRaises(errors.PluginError, self.postfix.restart)
|
||||
self.mock_call.assert_called_with(['start'])
|
||||
|
||||
class CheckAllOutputTest(unittest.TestCase):
|
||||
"""Tests for certbot_postfix.util.check_all_output."""
|
||||
|
||||
@classmethod
|
||||
def _call(cls, *args, **kwargs):
|
||||
from certbot_postfix.util import check_all_output
|
||||
return check_all_output(*args, **kwargs)
|
||||
|
||||
@mock.patch('certbot_postfix.util.logger')
|
||||
@mock.patch('certbot_postfix.util.subprocess.Popen')
|
||||
def test_command_error(self, mock_popen, mock_logger):
|
||||
command = 'foo'
|
||||
retcode = 42
|
||||
output = 'bar'
|
||||
err = 'baz'
|
||||
|
||||
mock_popen().communicate.return_value = (output, err)
|
||||
mock_popen().poll.return_value = 42
|
||||
|
||||
self.assertRaises(subprocess.CalledProcessError, self._call, command)
|
||||
log_args = mock_logger.debug.call_args[0]
|
||||
for value in (command, retcode, output, err,):
|
||||
self.assertTrue(value in log_args)
|
||||
|
||||
@mock.patch('certbot_postfix.util.subprocess.Popen')
|
||||
def test_success(self, mock_popen):
|
||||
command = 'foo'
|
||||
expected = ('bar', '')
|
||||
mock_popen().communicate.return_value = expected
|
||||
mock_popen().poll.return_value = 0
|
||||
|
||||
self.assertEqual(self._call(command), expected)
|
||||
|
||||
def test_stdout_error(self):
|
||||
self.assertRaises(ValueError, self._call, stdout=None)
|
||||
|
||||
def test_stderr_error(self):
|
||||
self.assertRaises(ValueError, self._call, stderr=None)
|
||||
|
||||
def test_universal_newlines_error(self):
|
||||
self.assertRaises(ValueError, self._call, universal_newlines=False)
|
||||
|
||||
|
||||
class VerifyExeExistsTest(unittest.TestCase):
|
||||
"""Tests for certbot_postfix.util.verify_exe_exists."""
|
||||
|
||||
@classmethod
|
||||
def _call(cls, *args, **kwargs):
|
||||
from certbot_postfix.util import verify_exe_exists
|
||||
return verify_exe_exists(*args, **kwargs)
|
||||
|
||||
@mock.patch('certbot_postfix.util.certbot_util.exe_exists')
|
||||
@mock.patch('certbot_postfix.util.plugins_util.path_surgery')
|
||||
def test_failure(self, mock_exe_exists, mock_path_surgery):
|
||||
mock_exe_exists.return_value = mock_path_surgery.return_value = False
|
||||
self.assertRaises(errors.NoInstallationError, self._call, 'foo')
|
||||
|
||||
@mock.patch('certbot_postfix.util.certbot_util.exe_exists')
|
||||
def test_simple_success(self, mock_exe_exists):
|
||||
mock_exe_exists.return_value = True
|
||||
self._call('foo')
|
||||
|
||||
@mock.patch('certbot_postfix.util.certbot_util.exe_exists')
|
||||
@mock.patch('certbot_postfix.util.plugins_util.path_surgery')
|
||||
def test_successful_surgery(self, mock_exe_exists, mock_path_surgery):
|
||||
mock_exe_exists.return_value = False
|
||||
mock_path_surgery.return_value = True
|
||||
self._call('foo')
|
||||
|
||||
class TestUtils(unittest.TestCase):
|
||||
""" Testing random utility functions in util.py
|
||||
"""
|
||||
def test_report_master_overrides(self):
|
||||
from certbot_postfix.util import report_master_overrides
|
||||
self.assertRaises(errors.PluginError, report_master_overrides, 'name',
|
||||
[('service/type', 'value')])
|
||||
# Shouldn't raise error
|
||||
report_master_overrides('name', [('service/type', 'value')],
|
||||
acceptable_overrides=('value',))
|
||||
|
||||
def test_no_acceptable_value(self):
|
||||
from certbot_postfix.util import is_acceptable_value
|
||||
self.assertFalse(is_acceptable_value('name', 'value', None))
|
||||
|
||||
def test_is_acceptable_value(self):
|
||||
from certbot_postfix.util import is_acceptable_value
|
||||
self.assertTrue(is_acceptable_value('name', 'value', ('value',)))
|
||||
self.assertFalse(is_acceptable_value('name', 'bad', ('value',)))
|
||||
|
||||
def test_is_acceptable_tuples(self):
|
||||
from certbot_postfix.util import is_acceptable_value
|
||||
self.assertTrue(is_acceptable_value('name', 'value', ('value', 'value1')))
|
||||
self.assertFalse(is_acceptable_value('name', 'bad', ('value', 'value1')))
|
||||
|
||||
def test_is_acceptable_protocols(self):
|
||||
from certbot_postfix.util import is_acceptable_value
|
||||
# SSLv2 and SSLv3 are both not supported, unambiguously
|
||||
self.assertFalse(is_acceptable_value('tls_mandatory_protocols_lol',
|
||||
'SSLv2, SSLv3', None))
|
||||
self.assertFalse(is_acceptable_value('tls_protocols_lol',
|
||||
'SSLv2, SSLv3', None))
|
||||
self.assertFalse(is_acceptable_value('tls_protocols_lol',
|
||||
'!SSLv2, !TLSv1', None))
|
||||
self.assertFalse(is_acceptable_value('tls_protocols_lol',
|
||||
'!SSLv2, SSLv3, !SSLv3, ', None))
|
||||
self.assertTrue(is_acceptable_value('tls_protocols_lol',
|
||||
'!SSLv2, !SSLv3', None))
|
||||
self.assertTrue(is_acceptable_value('tls_protocols_lol',
|
||||
'!SSLv3, !TLSv1, !SSLv2', None))
|
||||
# TLSv1.2 is supported unambiguously
|
||||
self.assertFalse(is_acceptable_value('tls_protocols_lol',
|
||||
'TLSv1, TLSv1.1,', None))
|
||||
self.assertFalse(is_acceptable_value('tls_protocols_lol',
|
||||
'TLSv1.2, !TLSv1.2,', None))
|
||||
self.assertTrue(is_acceptable_value('tls_protocols_lol',
|
||||
'TLSv1.2, ', None))
|
||||
self.assertTrue(is_acceptable_value('tls_protocols_lol',
|
||||
'TLSv1, TLSv1.1, TLSv1.2', None))
|
||||
|
||||
if __name__ == '__main__': # pragma: no cover
|
||||
unittest.main()
|
||||
292
certbot-postfix/certbot_postfix/util.py
Normal file
292
certbot-postfix/certbot_postfix/util.py
Normal file
@@ -0,0 +1,292 @@
|
||||
"""Utility functions for use in the Postfix installer."""
|
||||
import logging
|
||||
import re
|
||||
import subprocess
|
||||
|
||||
from certbot import errors
|
||||
from certbot import util as certbot_util
|
||||
from certbot.plugins import util as plugins_util
|
||||
from certbot_postfix import constants
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
COMMAND = "postfix"
|
||||
|
||||
class PostfixUtilBase(object):
|
||||
"""A base class for wrapping Postfix command line utilities."""
|
||||
|
||||
def __init__(self, executable, config_dir=None):
|
||||
"""Sets up the Postfix utility class.
|
||||
|
||||
:param str executable: name or path of the Postfix utility
|
||||
:param str config_dir: path to an alternative Postfix config
|
||||
|
||||
:raises .NoInstallationError: when the executable isn't found
|
||||
|
||||
"""
|
||||
self.executable = executable
|
||||
verify_exe_exists(executable)
|
||||
self._set_base_command(config_dir)
|
||||
self.config_dir = None
|
||||
|
||||
def _set_base_command(self, config_dir):
|
||||
self._base_command = [self.executable]
|
||||
if config_dir is not None:
|
||||
self._base_command.extend(('-c', config_dir,))
|
||||
|
||||
def _call(self, extra_args=None):
|
||||
"""Runs the Postfix utility and returns the result.
|
||||
|
||||
:param list extra_args: additional arguments for the command
|
||||
|
||||
:returns: data written to stdout and stderr
|
||||
:rtype: `tuple` of `str`
|
||||
|
||||
:raises subprocess.CalledProcessError: if the command fails
|
||||
|
||||
"""
|
||||
args = list(self._base_command)
|
||||
if extra_args is not None:
|
||||
args.extend(extra_args)
|
||||
return check_all_output(args)
|
||||
|
||||
def _get_output(self, extra_args=None):
|
||||
"""Runs the Postfix utility and returns only stdout output.
|
||||
|
||||
This function relies on self._call for running the utility.
|
||||
|
||||
:param list extra_args: additional arguments for the command
|
||||
|
||||
:returns: data written to stdout
|
||||
:rtype: str
|
||||
|
||||
:raises subprocess.CalledProcessError: if the command fails
|
||||
|
||||
"""
|
||||
return self._call(extra_args)[0]
|
||||
|
||||
class PostfixUtil(PostfixUtilBase):
|
||||
"""Wrapper around Postfix CLI tool.
|
||||
"""
|
||||
|
||||
def __init__(self, config_dir=None):
|
||||
super(PostfixUtil, self).__init__(COMMAND, config_dir)
|
||||
|
||||
def test(self):
|
||||
"""Make sure the configuration is valid.
|
||||
|
||||
:raises .MisconfigurationError: if the config is invalid
|
||||
"""
|
||||
try:
|
||||
self._call(["check"])
|
||||
except subprocess.CalledProcessError as e:
|
||||
logger.debug("Could not check postfix configuration:\n%s", e)
|
||||
raise errors.MisconfigurationError(
|
||||
"Postfix failed internal configuration check.")
|
||||
|
||||
def restart(self):
|
||||
"""Restart or refresh the server content.
|
||||
|
||||
:raises .PluginError: when server cannot be restarted
|
||||
|
||||
"""
|
||||
logger.info("Reloading Postfix configuration...")
|
||||
if self._is_running():
|
||||
self._reload()
|
||||
else:
|
||||
self._start()
|
||||
|
||||
|
||||
def _is_running(self):
|
||||
"""Is Postfix currently running?
|
||||
|
||||
Uses the 'postfix status' command to determine if Postfix is
|
||||
currently running using the specified configuration files.
|
||||
|
||||
:returns: True if Postfix is running, otherwise, False
|
||||
:rtype: bool
|
||||
|
||||
"""
|
||||
try:
|
||||
self._call(["status"])
|
||||
except subprocess.CalledProcessError:
|
||||
return False
|
||||
return True
|
||||
|
||||
def _start(self):
|
||||
"""Instructions Postfix to start running.
|
||||
|
||||
:raises .PluginError: when Postfix cannot start
|
||||
|
||||
"""
|
||||
try:
|
||||
self._call(["start"])
|
||||
except subprocess.CalledProcessError:
|
||||
raise errors.PluginError("Postfix failed to start")
|
||||
|
||||
def _reload(self):
|
||||
"""Instructs Postfix to reload its configuration.
|
||||
|
||||
If Postfix isn't currently running, this method will fail.
|
||||
|
||||
:raises .PluginError: when Postfix cannot reload
|
||||
"""
|
||||
try:
|
||||
self._call(["reload"])
|
||||
except subprocess.CalledProcessError:
|
||||
raise errors.PluginError(
|
||||
"Postfix failed to reload its configuration")
|
||||
|
||||
def check_all_output(*args, **kwargs):
|
||||
"""A version of subprocess.check_output that also captures stderr.
|
||||
|
||||
This is the same as :func:`subprocess.check_output` except output
|
||||
written to stderr is also captured and returned to the caller. The
|
||||
return value is a tuple of two strings (rather than byte strings).
|
||||
To accomplish this, the caller cannot set the stdout, stderr, or
|
||||
universal_newlines parameters to :class:`subprocess.Popen`.
|
||||
|
||||
Additionally, if the command exits with a nonzero status, output is
|
||||
not included in the raised :class:`subprocess.CalledProcessError`
|
||||
because Python 2.6 does not support this. Instead, the failure
|
||||
including the output is logged.
|
||||
|
||||
:param tuple args: positional arguments for Popen
|
||||
:param dict kwargs: keyword arguments for Popen
|
||||
|
||||
:returns: data written to stdout and stderr
|
||||
:rtype: `tuple` of `str`
|
||||
|
||||
:raises ValueError: if arguments are invalid
|
||||
:raises subprocess.CalledProcessError: if the command fails
|
||||
|
||||
"""
|
||||
for keyword in ('stdout', 'stderr', 'universal_newlines',):
|
||||
if keyword in kwargs:
|
||||
raise ValueError(
|
||||
keyword + ' argument not allowed, it will be overridden.')
|
||||
|
||||
kwargs['stdout'] = subprocess.PIPE
|
||||
kwargs['stderr'] = subprocess.PIPE
|
||||
kwargs['universal_newlines'] = True
|
||||
|
||||
process = subprocess.Popen(*args, **kwargs)
|
||||
output, err = process.communicate()
|
||||
retcode = process.poll()
|
||||
if retcode:
|
||||
cmd = kwargs.get('args')
|
||||
if cmd is None:
|
||||
cmd = args[0]
|
||||
logger.debug(
|
||||
"'%s' exited with %d. stdout output was:\n%s\nstderr output was:\n%s",
|
||||
cmd, retcode, output, err)
|
||||
raise subprocess.CalledProcessError(retcode, cmd)
|
||||
return (output, err)
|
||||
|
||||
|
||||
def verify_exe_exists(exe, message=None):
|
||||
"""Ensures an executable with the given name is available.
|
||||
|
||||
If an executable isn't found for the given path or name, extra
|
||||
directories are added to the user's PATH to help find system
|
||||
utilities that may not be available in the default cron PATH.
|
||||
|
||||
:param str exe: executable path or name
|
||||
:param str message: Error message to print.
|
||||
|
||||
:raises .NoInstallationError: when the executable isn't found
|
||||
|
||||
"""
|
||||
if message is None:
|
||||
message = "Cannot find executable '{0}'.".format(exe)
|
||||
if not (certbot_util.exe_exists(exe) or plugins_util.path_surgery(exe)):
|
||||
raise errors.NoInstallationError(message)
|
||||
|
||||
def report_master_overrides(name, overrides, acceptable_overrides=None):
|
||||
"""If the value for a parameter `name` is overridden by other services,
|
||||
report a warning to notify the user. If `parameter` is a TLS version parameter
|
||||
(i.e., `parameter` contains 'tls_protocols' or 'tls_mandatory_protocols'), then
|
||||
`acceptable_overrides` isn't used each value in overrides is inspected for secure TLS
|
||||
versions.
|
||||
|
||||
:param str name: The name of the parameter that is being overridden.
|
||||
:param list overrides: The values that other services are setting for `name`.
|
||||
Each override is a tuple: (service name, value)
|
||||
:param tuple acceptable_overrides: Override values that are acceptable. For instance, if
|
||||
another service is overriding our parameter with a more secure option, we don't have
|
||||
to warn. If this is set to None, errors are raised for *any* overrides of `name`!
|
||||
"""
|
||||
error_string = ""
|
||||
for override in overrides:
|
||||
service, value = override
|
||||
# If this override is acceptable:
|
||||
if acceptable_overrides is not None and \
|
||||
is_acceptable_value(name, value, acceptable_overrides):
|
||||
continue
|
||||
error_string += " {0}: {1}\n".format(service, value)
|
||||
if error_string:
|
||||
raise errors.PluginError("{0} is overridden with less secure options by the "
|
||||
"following services in master.cf:\n".format(name) + error_string)
|
||||
|
||||
|
||||
def is_acceptable_value(parameter, value, acceptable=None):
|
||||
""" Returns whether the `value` for this `parameter` is acceptable,
|
||||
given a tuple of `acceptable` values. If `parameter` is a TLS version parameter
|
||||
(i.e., `parameter` contains 'tls_protocols' or 'tls_mandatory_protocols'), then
|
||||
`acceptable` isn't used and `value` is inspected for secure TLS versions.
|
||||
|
||||
:param str parameter: The name of the parameter being set.
|
||||
:param str value: Proposed new value for parameter.
|
||||
:param tuple acceptable: List of acceptable values for parameter.
|
||||
"""
|
||||
# Check if param value is a comma-separated list of protocols.
|
||||
# Otherwise, just check whether the value is in the acceptable list.
|
||||
if 'tls_protocols' in parameter or 'tls_mandatory_protocols' in parameter:
|
||||
return _has_acceptable_tls_versions(value)
|
||||
if acceptable is not None:
|
||||
return value in acceptable
|
||||
return False
|
||||
|
||||
|
||||
def _has_acceptable_tls_versions(parameter_string):
|
||||
"""
|
||||
Checks to see if the list of TLS protocols is acceptable.
|
||||
This requires that TLSv1.2 is supported, and neither SSLv2 nor SSLv3 are supported.
|
||||
|
||||
Should be a string of protocol names delimited by commas, spaces, or colons.
|
||||
|
||||
Postfix's documents suggest listing protocols to exclude, like "!SSLv2, !SSLv3".
|
||||
Listing the protocols to include, like "TLSv1, TLSv1.1, TLSv1.2" is okay as well,
|
||||
though not recommended
|
||||
|
||||
When these two modes are interspersed, the presence of a single non-negated protocol name
|
||||
(i.e. "TLSv1" rather than "!TLSv1") automatically excludes all other unnamed protocols.
|
||||
|
||||
In addition, the presence of both a protocol name inclusion and exclusion isn't explicitly
|
||||
documented, so this method should return False if it encounters contradicting statements
|
||||
about TLSv1.2, SSLv2, or SSLv3. (for instance, "SSLv3, !SSLv3").
|
||||
"""
|
||||
if not parameter_string:
|
||||
return False
|
||||
bad_versions = list(constants.TLS_VERSIONS)
|
||||
for version in constants.ACCEPTABLE_TLS_VERSIONS:
|
||||
del bad_versions[bad_versions.index(version)]
|
||||
supported_version_list = re.split("[, :]+", parameter_string)
|
||||
# The presence of any non-"!" protocol listing excludes the others by default.
|
||||
inclusion_list = False
|
||||
for version in supported_version_list:
|
||||
if not version:
|
||||
continue
|
||||
if version in bad_versions: # short-circuit if we recognize any bad version
|
||||
return False
|
||||
if version[0] != "!":
|
||||
inclusion_list = True
|
||||
if inclusion_list: # For any inclusion list, we still require TLS 1.2.
|
||||
if "TLSv1.2" not in supported_version_list or "!TLSv1.2" in supported_version_list:
|
||||
return False
|
||||
else:
|
||||
for bad_version in bad_versions:
|
||||
if "!" + bad_version not in supported_version_list:
|
||||
return False
|
||||
return True
|
||||
|
||||
1
certbot-postfix/docs/.gitignore
vendored
Normal file
1
certbot-postfix/docs/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/_build/
|
||||
20
certbot-postfix/docs/Makefile
Normal file
20
certbot-postfix/docs/Makefile
Normal file
@@ -0,0 +1,20 @@
|
||||
# Minimal makefile for Sphinx documentation
|
||||
#
|
||||
|
||||
# You can set these variables from the command line.
|
||||
SPHINXOPTS =
|
||||
SPHINXBUILD = sphinx-build
|
||||
SPHINXPROJ = certbot-postfix
|
||||
SOURCEDIR = .
|
||||
BUILDDIR = _build
|
||||
|
||||
# Put it first so that "make" without argument is like "make help".
|
||||
help:
|
||||
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
||||
|
||||
.PHONY: help Makefile
|
||||
|
||||
# Catch-all target: route all unknown targets to Sphinx using the new
|
||||
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
|
||||
%: Makefile
|
||||
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
||||
8
certbot-postfix/docs/api.rst
Normal file
8
certbot-postfix/docs/api.rst
Normal file
@@ -0,0 +1,8 @@
|
||||
=================
|
||||
API Documentation
|
||||
=================
|
||||
|
||||
.. toctree::
|
||||
:glob:
|
||||
|
||||
api/**
|
||||
5
certbot-postfix/docs/api/installer.rst
Normal file
5
certbot-postfix/docs/api/installer.rst
Normal file
@@ -0,0 +1,5 @@
|
||||
:mod:`certbot_postfix.installer`
|
||||
--------------------------------------
|
||||
|
||||
.. automodule:: certbot_postfix.installer
|
||||
:members:
|
||||
5
certbot-postfix/docs/api/postconf.rst
Normal file
5
certbot-postfix/docs/api/postconf.rst
Normal file
@@ -0,0 +1,5 @@
|
||||
:mod:`certbot_postfix.postconf`
|
||||
-------------------------------
|
||||
|
||||
.. automodule:: certbot_postfix.postconf
|
||||
:members:
|
||||
190
certbot-postfix/docs/conf.py
Normal file
190
certbot-postfix/docs/conf.py
Normal file
@@ -0,0 +1,190 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Configuration file for the Sphinx documentation builder.
|
||||
#
|
||||
# This file does only contain a selection of the most common options. For a
|
||||
# full list see the documentation:
|
||||
# http://www.sphinx-doc.org/en/master/config
|
||||
|
||||
# -- Path setup --------------------------------------------------------------
|
||||
|
||||
# If extensions (or modules to document with autodoc) are in another directory,
|
||||
# add these directories to sys.path here. If the directory is relative to the
|
||||
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||
#
|
||||
import os
|
||||
# import sys
|
||||
# sys.path.insert(0, os.path.abspath('.'))
|
||||
|
||||
|
||||
# -- Project information -----------------------------------------------------
|
||||
|
||||
project = u'certbot-postfix'
|
||||
copyright = u'2018, Certbot Project'
|
||||
author = u'Certbot Project'
|
||||
|
||||
# The short X.Y version
|
||||
version = u'0'
|
||||
# The full version, including alpha/beta/rc tags
|
||||
release = u'0'
|
||||
|
||||
|
||||
# -- General configuration ---------------------------------------------------
|
||||
|
||||
# If your documentation needs a minimal Sphinx version, state it here.
|
||||
#
|
||||
needs_sphinx = '1.0'
|
||||
|
||||
# Add any Sphinx extension module names here, as strings. They can be
|
||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
||||
# ones.
|
||||
extensions = [
|
||||
'sphinx.ext.autodoc',
|
||||
'sphinx.ext.intersphinx',
|
||||
'sphinx.ext.todo',
|
||||
'sphinx.ext.coverage',
|
||||
'sphinx.ext.viewcode',
|
||||
]
|
||||
|
||||
autodoc_member_order = 'bysource'
|
||||
autodoc_default_flags = ['show-inheritance', 'private-members']
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
templates_path = ['_templates']
|
||||
|
||||
# The suffix(es) of source filenames.
|
||||
# You can specify multiple suffix as a list of string:
|
||||
#
|
||||
# source_suffix = ['.rst', '.md']
|
||||
source_suffix = '.rst'
|
||||
|
||||
# The master toctree document.
|
||||
master_doc = 'index'
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
#
|
||||
# This is also used if you do content translation via gettext catalogs.
|
||||
# Usually you set "language" from the command line for these cases.
|
||||
language = u'en'
|
||||
|
||||
# List of patterns, relative to source directory, that match files and
|
||||
# directories to ignore when looking for source files.
|
||||
# This pattern also affects html_static_path and html_extra_path .
|
||||
exclude_patterns = [u'_build', 'Thumbs.db', '.DS_Store']
|
||||
|
||||
default_role = 'py:obj'
|
||||
|
||||
# The name of the Pygments (syntax highlighting) style to use.
|
||||
pygments_style = 'sphinx'
|
||||
|
||||
|
||||
# -- Options for HTML output -------------------------------------------------
|
||||
|
||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||
# a list of builtin themes.
|
||||
#
|
||||
|
||||
# http://docs.readthedocs.org/en/latest/theme.html#how-do-i-use-this-locally-and-on-read-the-docs
|
||||
# on_rtd is whether we are on readthedocs.org
|
||||
on_rtd = os.environ.get('READTHEDOCS', None) == 'True'
|
||||
if not on_rtd: # only import and set the theme if we're building docs locally
|
||||
import sphinx_rtd_theme
|
||||
html_theme = 'sphinx_rtd_theme'
|
||||
html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
|
||||
# otherwise, readthedocs.org uses their theme by default, so no need to specify it
|
||||
|
||||
# Theme options are theme-specific and customize the look and feel of a theme
|
||||
# further. For a list of options available for each theme, see the
|
||||
# documentation.
|
||||
#
|
||||
# html_theme_options = {}
|
||||
|
||||
# Add any paths that contain custom static files (such as style sheets) here,
|
||||
# relative to this directory. They are copied after the builtin static files,
|
||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||
# html_static_path = ['_static']
|
||||
|
||||
# Custom sidebar templates, must be a dictionary that maps document names
|
||||
# to template names.
|
||||
#
|
||||
# The default sidebars (for documents that don't match any pattern) are
|
||||
# defined by theme itself. Builtin themes are using these templates by
|
||||
# default: ``['localtoc.html', 'relations.html', 'sourcelink.html',
|
||||
# 'searchbox.html']``.
|
||||
#
|
||||
# html_sidebars = {}
|
||||
|
||||
|
||||
# -- Options for HTMLHelp output ---------------------------------------------
|
||||
|
||||
# Output file base name for HTML help builder.
|
||||
htmlhelp_basename = 'certbot-postfixdoc'
|
||||
|
||||
|
||||
# -- Options for LaTeX output ------------------------------------------------
|
||||
|
||||
latex_elements = {
|
||||
# The paper size ('letterpaper' or 'a4paper').
|
||||
#
|
||||
# 'papersize': 'letterpaper',
|
||||
|
||||
# The font size ('10pt', '11pt' or '12pt').
|
||||
#
|
||||
# 'pointsize': '10pt',
|
||||
|
||||
# Additional stuff for the LaTeX preamble.
|
||||
#
|
||||
# 'preamble': '',
|
||||
|
||||
# Latex figure (float) alignment
|
||||
#
|
||||
# 'figure_align': 'htbp',
|
||||
}
|
||||
|
||||
# Grouping the document tree into LaTeX files. List of tuples
|
||||
# (source start file, target name, title,
|
||||
# author, documentclass [howto, manual, or own class]).
|
||||
latex_documents = [
|
||||
(master_doc, 'certbot-postfix.tex', u'certbot-postfix Documentation',
|
||||
u'Certbot Project', 'manual'),
|
||||
]
|
||||
|
||||
|
||||
# -- Options for manual page output ------------------------------------------
|
||||
|
||||
# One entry per manual page. List of tuples
|
||||
# (source start file, name, description, authors, manual section).
|
||||
man_pages = [
|
||||
(master_doc, 'certbot-postfix', u'certbot-postfix Documentation',
|
||||
[author], 1)
|
||||
]
|
||||
|
||||
|
||||
# -- Options for Texinfo output ----------------------------------------------
|
||||
|
||||
# Grouping the document tree into Texinfo files. List of tuples
|
||||
# (source start file, target name, title, author,
|
||||
# dir menu entry, description, category)
|
||||
texinfo_documents = [
|
||||
(master_doc, 'certbot-postfix', u'certbot-postfix Documentation',
|
||||
author, 'certbot-postfix', 'One line description of project.',
|
||||
'Miscellaneous'),
|
||||
]
|
||||
|
||||
|
||||
# -- Extension configuration -------------------------------------------------
|
||||
|
||||
# -- Options for intersphinx extension ---------------------------------------
|
||||
|
||||
# Example configuration for intersphinx: refer to the Python standard library.
|
||||
intersphinx_mapping = {
|
||||
'python': ('https://docs.python.org/', None),
|
||||
'acme': ('https://acme-python.readthedocs.org/en/latest/', None),
|
||||
'certbot': ('https://certbot.eff.org/docs/', None),
|
||||
}
|
||||
|
||||
# -- Options for todo extension ----------------------------------------------
|
||||
|
||||
# If true, `todo` and `todoList` produce output, else they produce nothing.
|
||||
todo_include_todos = True
|
||||
28
certbot-postfix/docs/index.rst
Normal file
28
certbot-postfix/docs/index.rst
Normal file
@@ -0,0 +1,28 @@
|
||||
.. certbot-postfix documentation master file, created by
|
||||
sphinx-quickstart on Wed May 2 16:01:06 2018.
|
||||
You can adapt this file completely to your liking, but it should at least
|
||||
contain the root `toctree` directive.
|
||||
|
||||
Welcome to certbot-postfix's documentation!
|
||||
===========================================
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
:caption: Contents:
|
||||
|
||||
.. automodule:: certbot_postfix
|
||||
:members:
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
api
|
||||
|
||||
|
||||
|
||||
Indices and tables
|
||||
==================
|
||||
|
||||
* :ref:`genindex`
|
||||
* :ref:`modindex`
|
||||
* :ref:`search`
|
||||
36
certbot-postfix/docs/make.bat
Normal file
36
certbot-postfix/docs/make.bat
Normal file
@@ -0,0 +1,36 @@
|
||||
@ECHO OFF
|
||||
|
||||
pushd %~dp0
|
||||
|
||||
REM Command file for Sphinx documentation
|
||||
|
||||
if "%SPHINXBUILD%" == "" (
|
||||
set SPHINXBUILD=sphinx-build
|
||||
)
|
||||
set SOURCEDIR=.
|
||||
set BUILDDIR=_build
|
||||
set SPHINXPROJ=certbot-postfix
|
||||
|
||||
if "%1" == "" goto help
|
||||
|
||||
%SPHINXBUILD% >NUL 2>NUL
|
||||
if errorlevel 9009 (
|
||||
echo.
|
||||
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
|
||||
echo.installed, then set the SPHINXBUILD environment variable to point
|
||||
echo.to the full path of the 'sphinx-build' executable. Alternatively you
|
||||
echo.may add the Sphinx directory to PATH.
|
||||
echo.
|
||||
echo.If you don't have Sphinx installed, grab it from
|
||||
echo.http://sphinx-doc.org/
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
|
||||
goto end
|
||||
|
||||
:help
|
||||
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
|
||||
|
||||
:end
|
||||
popd
|
||||
2
certbot-postfix/local-oldest-requirements.txt
Normal file
2
certbot-postfix/local-oldest-requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
acme[dev]==0.25.0
|
||||
certbot[dev]==0.23.0
|
||||
2
certbot-postfix/setup.cfg
Normal file
2
certbot-postfix/setup.cfg
Normal file
@@ -0,0 +1,2 @@
|
||||
[bdist_wheel]
|
||||
universal = 1
|
||||
64
certbot-postfix/setup.py
Normal file
64
certbot-postfix/setup.py
Normal file
@@ -0,0 +1,64 @@
|
||||
from setuptools import setup
|
||||
from setuptools import find_packages
|
||||
|
||||
|
||||
version = '0.26.0.dev0'
|
||||
|
||||
install_requires = [
|
||||
'acme>=0.25.0',
|
||||
'certbot>=0.23.0',
|
||||
'setuptools',
|
||||
'six',
|
||||
'zope.component',
|
||||
'zope.interface',
|
||||
]
|
||||
|
||||
docs_extras = [
|
||||
'Sphinx>=1.0', # autodoc_member_order = 'bysource', autodoc_default_flags
|
||||
'sphinx_rtd_theme',
|
||||
]
|
||||
|
||||
setup(
|
||||
name='certbot-postfix',
|
||||
version=version,
|
||||
description="Postfix plugin for Certbot",
|
||||
url='https://github.com/certbot/certbot',
|
||||
author="Certbot Project",
|
||||
author_email='client-dev@letsencrypt.org',
|
||||
license='Apache License 2.0',
|
||||
python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*',
|
||||
classifiers=[
|
||||
'Development Status :: 3 - Alpha',
|
||||
'Environment :: Plugins',
|
||||
'Intended Audience :: System Administrators',
|
||||
'License :: OSI Approved :: Apache Software License',
|
||||
'Operating System :: POSIX :: Linux',
|
||||
'Programming Language :: Python',
|
||||
'Programming Language :: Python :: 2',
|
||||
'Programming Language :: Python :: 2.7',
|
||||
'Programming Language :: Python :: 3',
|
||||
'Programming Language :: Python :: 3.4',
|
||||
'Programming Language :: Python :: 3.5',
|
||||
'Programming Language :: Python :: 3.6',
|
||||
'Programming Language :: Python :: 3.7',
|
||||
'Topic :: Communications :: Email :: Mail Transport Agents',
|
||||
'Topic :: Security',
|
||||
'Topic :: System :: Installation/Setup',
|
||||
'Topic :: System :: Networking',
|
||||
'Topic :: System :: Systems Administration',
|
||||
'Topic :: Utilities',
|
||||
],
|
||||
|
||||
packages=find_packages(),
|
||||
include_package_data=True,
|
||||
install_requires=install_requires,
|
||||
extras_require={
|
||||
'docs': docs_extras,
|
||||
},
|
||||
entry_points={
|
||||
'certbot.plugins': [
|
||||
'postfix = certbot_postfix:Installer',
|
||||
],
|
||||
},
|
||||
test_suite='certbot_postfix',
|
||||
)
|
||||
@@ -175,33 +175,43 @@ 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)
|
||||
except OSError:
|
||||
return []
|
||||
prev_account_dir = self.config.accounts_dir_for_server_path(prev_server_path)
|
||||
os.symlink(prev_account_dir, accounts_dir)
|
||||
try:
|
||||
self._symlink_to_accounts_dir(prev_server_path, server_path)
|
||||
except OSError:
|
||||
return []
|
||||
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):
|
||||
raise errors.AccountNotFound(
|
||||
"Account at %s does not exist" % 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)
|
||||
|
||||
try:
|
||||
with open(self._regr_path(account_dir_path)) as regr_file:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -620,6 +620,9 @@ class GenericUpdater(object):
|
||||
methods, and interfaces.GenericUpdater.register(InstallerClass) should
|
||||
be called from the installer code.
|
||||
|
||||
The plugins implementing this enhancement are responsible of handling
|
||||
the saving of configuration checkpoints as well as other calls to
|
||||
interface methods of `interfaces.IInstaller` such as prepare() and restart()
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
|
||||
@@ -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.")
|
||||
|
||||
lineage = cert_manager.lineage_for_certname(config, config.certname)
|
||||
if not config.chain_path:
|
||||
lineage = cert_manager.lineage_for_certname(config, config.certname)
|
||||
config.chain_path = lineage.chain_path
|
||||
le_client = _init_le_client(config, authenticator=None, installer=installer)
|
||||
le_client.enhance_config(domains, config.chain_path, ask_redirect=False)
|
||||
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:
|
||||
@@ -1163,11 +1199,11 @@ def renew_cert(config, plugins, lineage):
|
||||
# In case of a renewal, reload server to pick up new certificate.
|
||||
# In principle we could have a configuration option to inhibit this
|
||||
# from happening.
|
||||
# Run deployer
|
||||
updater.run_renewal_deployer(config, renewed_lineage, installer)
|
||||
installer.restart()
|
||||
notify("new certificate deployed with reload of {0} server; fullchain is {1}".format(
|
||||
config.installer, lineage.fullchain), pause=False)
|
||||
# Run deployer
|
||||
|
||||
def certonly(config, plugins):
|
||||
"""Authenticate & obtain cert, but do not install it.
|
||||
|
||||
@@ -36,6 +36,7 @@ class PluginEntryPoint(object):
|
||||
"certbot-dns-rfc2136",
|
||||
"certbot-dns-route53",
|
||||
"certbot-nginx",
|
||||
"certbot-postfix",
|
||||
]
|
||||
"""Distributions for which prefix will be omitted."""
|
||||
|
||||
|
||||
164
certbot/plugins/enhancements.py
Normal file
164
certbot/plugins/enhancements.py
Normal file
@@ -0,0 +1,164 @@
|
||||
"""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. For update_autohsts method, the installer may
|
||||
have to call prepare() to finalize the plugin initialization.
|
||||
|
||||
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
|
||||
|
||||
.. note:: prepare() method inherited from `interfaces.IPlugin` might need
|
||||
to be called manually within implementation of this interface method
|
||||
to finalize the plugin initialization.
|
||||
"""
|
||||
|
||||
@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
|
||||
@@ -39,6 +39,35 @@ def pick_authenticator(
|
||||
return pick_plugin(
|
||||
config, default, plugins, question, (interfaces.IAuthenticator,))
|
||||
|
||||
def get_unprepared_installer(config, plugins):
|
||||
"""
|
||||
Get an unprepared interfaces.IInstaller object.
|
||||
|
||||
:param certbot.interfaces.IConfig config: Configuration
|
||||
:param certbot.plugins.disco.PluginsRegistry plugins:
|
||||
All plugins registered as entry points.
|
||||
|
||||
:returns: Unprepared installer plugin or None
|
||||
:rtype: IPlugin or None
|
||||
"""
|
||||
|
||||
_, req_inst = cli_plugin_requests(config)
|
||||
if not req_inst:
|
||||
return None
|
||||
installers = plugins.filter(lambda p_ep: p_ep.name == req_inst)
|
||||
installers.init(config)
|
||||
installers = installers.verify((interfaces.IInstaller,))
|
||||
if len(installers) > 1:
|
||||
raise errors.PluginSelectionError(
|
||||
"Found multiple installers with the name %s, Certbot is unable to "
|
||||
"determine which one to use. Skipping." % req_inst)
|
||||
if installers:
|
||||
inst = list(installers.values())[0]
|
||||
logger.debug("Selecting plugin: %s", inst)
|
||||
return inst.init(config)
|
||||
else:
|
||||
raise errors.PluginSelectionError(
|
||||
"Could not select or initialize the requested installer %s." % req_inst)
|
||||
|
||||
def pick_plugin(config, default, plugins, question, ifaces):
|
||||
"""Pick plugin.
|
||||
@@ -234,6 +263,10 @@ def cli_plugin_requests(config): # pylint: disable=too-many-branches
|
||||
:rtype: tuple
|
||||
"""
|
||||
req_inst = req_auth = config.configurator
|
||||
if config.installer == 'None':
|
||||
config.installer = None
|
||||
if config.authenticator == 'None':
|
||||
config.authenticator = None
|
||||
req_inst = set_configurator(req_inst, config.installer)
|
||||
req_auth = set_configurator(req_auth, config.authenticator)
|
||||
|
||||
|
||||
@@ -6,10 +6,13 @@ import unittest
|
||||
import mock
|
||||
import zope.component
|
||||
|
||||
from certbot import errors
|
||||
from certbot import interfaces
|
||||
|
||||
from acme.magic_typing import List # pylint: disable=unused-import, no-name-in-module
|
||||
from certbot.display import util as display_util
|
||||
from certbot.plugins.disco import PluginsRegistry
|
||||
from certbot.tests import util as test_util
|
||||
from certbot import interfaces
|
||||
|
||||
|
||||
class ConveniencePickPluginTest(unittest.TestCase):
|
||||
@@ -170,5 +173,56 @@ class ChoosePluginTest(unittest.TestCase):
|
||||
|
||||
self.assertTrue("default" in mock_util().menu.call_args[1])
|
||||
|
||||
class GetUnpreparedInstallerTest(test_util.ConfigTestCase):
|
||||
"""Tests for certbot.plugins.selection.get_unprepared_installer."""
|
||||
|
||||
def setUp(self):
|
||||
super(GetUnpreparedInstallerTest, self).setUp()
|
||||
self.mock_apache_fail_ep = mock.Mock(
|
||||
description_with_name="afail")
|
||||
self.mock_apache_fail_ep.name = "afail"
|
||||
self.mock_apache_ep = mock.Mock(
|
||||
description_with_name="apache")
|
||||
self.mock_apache_ep.name = "apache"
|
||||
self.mock_apache_plugin = mock.MagicMock()
|
||||
self.mock_apache_ep.init.return_value = self.mock_apache_plugin
|
||||
self.plugins = PluginsRegistry({
|
||||
"afail": self.mock_apache_fail_ep,
|
||||
"apache": self.mock_apache_ep,
|
||||
})
|
||||
|
||||
def _call(self):
|
||||
from certbot.plugins.selection import get_unprepared_installer
|
||||
return get_unprepared_installer(self.config, self.plugins)
|
||||
|
||||
def test_no_installer_defined(self):
|
||||
self.config.configurator = None
|
||||
self.assertEquals(self._call(), None)
|
||||
|
||||
def test_no_available_installers(self):
|
||||
self.config.configurator = "apache"
|
||||
self.plugins = PluginsRegistry({})
|
||||
self.assertRaises(errors.PluginSelectionError, self._call)
|
||||
|
||||
def test_get_plugin(self):
|
||||
self.config.configurator = "apache"
|
||||
installer = self._call()
|
||||
self.assertTrue(installer is self.mock_apache_plugin)
|
||||
|
||||
def test_multiple_installers_returned(self):
|
||||
self.config.configurator = "apache"
|
||||
# Two plugins with the same name
|
||||
self.mock_apache_fail_ep.name = "apache"
|
||||
self.assertRaises(errors.PluginSelectionError, self._call)
|
||||
|
||||
def test_return_early_if_none(self):
|
||||
self.config.installer = 'None'
|
||||
# Make sure that the function returns early. PluginsRegistry.filter is
|
||||
# called right after we should return.
|
||||
with mock.patch('certbot.plugins.disco.PluginsRegistry.filter') as mock_f:
|
||||
self._call()
|
||||
self.assertFalse(mock_f.called)
|
||||
|
||||
|
||||
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):
|
||||
@@ -1476,17 +1493,17 @@ class MainTest(test_util.ConfigTestCase): # pylint: disable=too-many-public-met
|
||||
self.assertTrue(mock_handle.called)
|
||||
|
||||
@mock.patch('certbot.plugins.selection.choose_configurator_plugins')
|
||||
def test_plugin_selection_error(self, mock_choose):
|
||||
@mock.patch('certbot.updater._run_updaters')
|
||||
def test_plugin_selection_error(self, mock_run, mock_choose):
|
||||
mock_choose.side_effect = errors.PluginSelectionError
|
||||
self.assertRaises(errors.PluginSelectionError, main.renew_cert,
|
||||
None, None, None)
|
||||
|
||||
with mock.patch('certbot.updater.logger.warning') as mock_log:
|
||||
self.config.dry_run = False
|
||||
updater.run_generic_updaters(self.config, None, None)
|
||||
self.assertTrue(mock_log.called)
|
||||
self.assertTrue("Could not choose appropriate plugin for updaters"
|
||||
in mock_log.call_args[0][0])
|
||||
self.config.dry_run = False
|
||||
updater.run_generic_updaters(self.config, None, None)
|
||||
# Make sure we're returning None, and hence not trying to run the
|
||||
# without installer
|
||||
self.assertFalse(mock_run.called)
|
||||
|
||||
|
||||
class UnregisterTest(unittest.TestCase):
|
||||
@@ -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,50 +16,37 @@ 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')
|
||||
@mock.patch('certbot.plugins.selection.get_unprepared_installer')
|
||||
@test_util.patch_get_utility()
|
||||
def test_server_updates(self, _, mock_select, mock_getsave):
|
||||
def test_server_updates(self, _, mock_geti, mock_select, mock_getsave):
|
||||
mock_getsave.return_value = mock.MagicMock()
|
||||
mock_generic_updater = self.generic_updater
|
||||
|
||||
# Generic Updater
|
||||
mock_select.return_value = (mock_generic_updater, None)
|
||||
mock_geti.return_value = mock_generic_updater
|
||||
with mock.patch('certbot.main._init_le_client'):
|
||||
main.renew_cert(self.config, None, mock.MagicMock())
|
||||
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 +64,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.get_unprepared_installer')
|
||||
def test_enhancement_updates(self, mock_geti):
|
||||
mock_geti.return_value = self.mockinstaller
|
||||
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.get_unprepared_installer')
|
||||
def test_enhancement_updates_not_called(self, mock_geti):
|
||||
self.config.disable_renew_updates = True
|
||||
mock_geti.return_value = self.mockinstaller
|
||||
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.get_unprepared_installer')
|
||||
def test_enhancement_no_updater(self, mock_geti):
|
||||
FAKEINDEX = [
|
||||
{
|
||||
"name": "Test",
|
||||
"class": enhancements.AutoHSTSEnhancement,
|
||||
"updater_function": None,
|
||||
"deployer_function": "deploy_autohsts",
|
||||
"enable_function": "enable_autohsts"
|
||||
}
|
||||
]
|
||||
mock_geti.return_value = self.mockinstaller
|
||||
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__)
|
||||
|
||||
@@ -27,12 +28,13 @@ def run_generic_updaters(config, lineage, plugins):
|
||||
logger.debug("Skipping updaters in dry-run mode.")
|
||||
return
|
||||
try:
|
||||
# installers are used in auth mode to determine domain names
|
||||
installer, _ = plug_sel.choose_configurator_plugins(config, plugins, "certonly")
|
||||
except errors.PluginSelectionError as e:
|
||||
installer = plug_sel.get_unprepared_installer(config, plugins)
|
||||
except errors.Error as e:
|
||||
logger.warning("Could not choose appropriate plugin for updaters: %s", e)
|
||||
return
|
||||
_run_updaters(lineage, installer, config)
|
||||
if installer:
|
||||
_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.25.1 (certbot;
|
||||
darwin 10.13.5) Authenticator/XXX Installer/YYY
|
||||
(SUBCOMMAND; flags: FLAGS) Py/2.7.15). 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
|
||||
@@ -638,7 +638,7 @@ nginx:
|
||||
Nginx Web Server plugin - Alpha
|
||||
|
||||
--nginx-server-root NGINX_SERVER_ROOT
|
||||
Nginx server root directory. (default:
|
||||
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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -1103,9 +1092,9 @@ idna==2.5 \
|
||||
ipaddress==1.0.16 \
|
||||
--hash=sha256:935712800ce4760701d89ad677666cd52691fd2f6f0b340c8b4239a3c17988a5 \
|
||||
--hash=sha256:5a3182b322a706525c46282ca6f064d27a02cffbd449f9f47416f1dc96aa71b0
|
||||
josepy==1.0.1 \
|
||||
--hash=sha256:354a3513038a38bbcd27c97b7c68a8f3dfaff0a135b20a92c6db4cc4ea72915e \
|
||||
--hash=sha256:9f48b88ca37f0244238b1cc77723989f7c54f7b90b2eee6294390bacfe870acc
|
||||
josepy==1.1.0 \
|
||||
--hash=sha256:1309a25aac3caeff5239729c58ff9b583f7d022ffdb1553406ddfc8e5b52b76e \
|
||||
--hash=sha256:fb5c62c77d26e04df29cb5ecd01b9ce69b6fcc9e521eb1ca193b7faa2afa7086
|
||||
linecache2==1.0.0 \
|
||||
--hash=sha256:e78be9c0a0dfcbac712fe04fbf92b96cddae80b1b842f24248214c8496f006ef \
|
||||
--hash=sha256:4b26ff4e7110db76eeb6f5a7b64a82623839d595c2038eeda662f2a2db78e97c
|
||||
|
||||
@@ -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
|
||||
@@ -107,9 +96,9 @@ idna==2.5 \
|
||||
ipaddress==1.0.16 \
|
||||
--hash=sha256:935712800ce4760701d89ad677666cd52691fd2f6f0b340c8b4239a3c17988a5 \
|
||||
--hash=sha256:5a3182b322a706525c46282ca6f064d27a02cffbd449f9f47416f1dc96aa71b0
|
||||
josepy==1.0.1 \
|
||||
--hash=sha256:354a3513038a38bbcd27c97b7c68a8f3dfaff0a135b20a92c6db4cc4ea72915e \
|
||||
--hash=sha256:9f48b88ca37f0244238b1cc77723989f7c54f7b90b2eee6294390bacfe870acc
|
||||
josepy==1.1.0 \
|
||||
--hash=sha256:1309a25aac3caeff5239729c58ff9b583f7d022ffdb1553406ddfc8e5b52b76e \
|
||||
--hash=sha256:fb5c62c77d26e04df29cb5ecd01b9ce69b6fcc9e521eb1ca193b7faa2afa7086
|
||||
linecache2==1.0.0 \
|
||||
--hash=sha256:e78be9c0a0dfcbac712fe04fbf92b96cddae80b1b842f24248214c8496f006ef \
|
||||
--hash=sha256:4b26ff4e7110db76eeb6f5a7b64a82623839d595c2038eeda662f2a2db78e97c
|
||||
|
||||
@@ -35,6 +35,7 @@ setup(
|
||||
'Programming Language :: Python :: 3.4',
|
||||
'Programming Language :: Python :: 3.5',
|
||||
'Programming Language :: Python :: 3.6',
|
||||
'Programming Language :: Python :: 3.7',
|
||||
'Topic :: Internet :: WWW/HTTP',
|
||||
'Topic :: Security',
|
||||
'Topic :: System :: Installation/Setup',
|
||||
|
||||
@@ -1 +1 @@
|
||||
-e acme[dev]
|
||||
acme[dev]==0.25.0
|
||||
|
||||
6
setup.py
6
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.
|
||||
@@ -100,6 +99,7 @@ setup(
|
||||
'Programming Language :: Python :: 3.4',
|
||||
'Programming Language :: Python :: 3.5',
|
||||
'Programming Language :: Python :: 3.6',
|
||||
'Programming Language :: Python :: 3.7',
|
||||
'Topic :: Internet :: WWW/HTTP',
|
||||
'Topic :: Security',
|
||||
'Topic :: System :: Installation/Setup',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -26,7 +26,7 @@ ipython==5.5.0
|
||||
ipython-genutils==0.2.0
|
||||
Jinja2==2.9.6
|
||||
jmespath==0.9.3
|
||||
josepy==1.0.1
|
||||
josepy==1.1.0
|
||||
logger==1.4
|
||||
logilab-common==1.4.1
|
||||
MarkupSafe==1.0
|
||||
@@ -51,7 +51,7 @@ pytest-forked==0.2
|
||||
pytest-xdist==1.20.1
|
||||
python-dateutil==2.6.1
|
||||
python-digitalocean==1.11
|
||||
PyYAML==3.12
|
||||
PyYAML==3.13
|
||||
repoze.sphinx.autointerface==0.8
|
||||
requests-file==1.4.2
|
||||
requests-toolbelt==0.8.0
|
||||
|
||||
@@ -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 ..
|
||||
|
||||
@@ -25,5 +25,6 @@ fi
|
||||
-e certbot-dns-rfc2136 \
|
||||
-e certbot-dns-route53 \
|
||||
-e certbot-nginx \
|
||||
-e certbot-postfix \
|
||||
-e letshelp-certbot \
|
||||
-e certbot-compatibility-test
|
||||
|
||||
@@ -23,5 +23,6 @@ fi
|
||||
-e certbot-dns-nsone \
|
||||
-e certbot-dns-route53 \
|
||||
-e certbot-nginx \
|
||||
-e certbot-postfix \
|
||||
-e letshelp-certbot \
|
||||
-e certbot-compatibility-test
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
# -e makes sure we fail fast and don't submit coveralls submit
|
||||
|
||||
if [ "xxx$1" = "xxx" ]; then
|
||||
pkgs="certbot acme certbot_apache certbot_dns_cloudflare certbot_dns_cloudxns certbot_dns_digitalocean certbot_dns_dnsimple certbot_dns_dnsmadeeasy certbot_dns_google certbot_dns_luadns certbot_dns_nsone certbot_dns_rfc2136 certbot_dns_route53 certbot_nginx letshelp_certbot"
|
||||
pkgs="certbot acme certbot_apache certbot_dns_cloudflare certbot_dns_cloudxns certbot_dns_digitalocean certbot_dns_dnsimple certbot_dns_dnsmadeeasy certbot_dns_google certbot_dns_luadns certbot_dns_nsone certbot_dns_rfc2136 certbot_dns_route53 certbot_nginx certbot_postfix letshelp_certbot"
|
||||
else
|
||||
pkgs="$@"
|
||||
fi
|
||||
@@ -43,6 +43,8 @@ cover () {
|
||||
min=92
|
||||
elif [ "$1" = "certbot_nginx" ]; then
|
||||
min=97
|
||||
elif [ "$1" = "certbot_postfix" ]; then
|
||||
min=100
|
||||
elif [ "$1" = "letshelp_certbot" ]; then
|
||||
min=100
|
||||
else
|
||||
|
||||
2
tox.ini
2
tox.ini
@@ -31,6 +31,7 @@ all_packages =
|
||||
certbot-apache \
|
||||
{[base]dns_packages} \
|
||||
certbot-nginx \
|
||||
certbot-postfix \
|
||||
letshelp-certbot
|
||||
install_packages =
|
||||
{toxinidir}/tools/pip_install_editable.sh {[base]all_packages}
|
||||
@@ -50,6 +51,7 @@ source_paths =
|
||||
certbot-dns-rfc2136/certbot_dns_rfc2136
|
||||
certbot-dns-route53/certbot_dns_route53
|
||||
certbot-nginx/certbot_nginx
|
||||
certbot-postfix/certbot_postfix
|
||||
letshelp-certbot/letshelp_certbot
|
||||
tests/lock_test.py
|
||||
|
||||
|
||||
Reference in New Issue
Block a user