mirror of
https://github.com/certbot/certbot.git
synced 2026-01-21 19:01:07 +03:00
merge with letsencrypt master br
This commit is contained in:
10
.travis.yml
10
.travis.yml
@@ -2,11 +2,12 @@ language: python
|
||||
|
||||
services:
|
||||
- rabbitmq
|
||||
- mariadb
|
||||
|
||||
# http://docs.travis-ci.com/user/ci-environment/#CI-environment-OS
|
||||
# gimme has to be kept in sync with Boulder's Go version setting in .travis.yml
|
||||
before_install:
|
||||
- sudo apt-get install -y mariadb-server mariadb-server-10.0
|
||||
- 'dpkg -s libaugeas0'
|
||||
- '[ "xxx$BOULDER_INTEGRATION" = "xxx" ] || eval "$(gimme 1.5.1)"'
|
||||
|
||||
# using separate envs with different TOXENVs creates 4x1 Travis build
|
||||
@@ -31,9 +32,8 @@ branches:
|
||||
- master
|
||||
- /^test-.*$/
|
||||
|
||||
# enable Trusty beta on travis
|
||||
sudo: required
|
||||
dist: trusty
|
||||
# container-based infrastructure
|
||||
sudo: false
|
||||
|
||||
addons:
|
||||
# make sure simplehttp simple verification works (custom /etc/hosts)
|
||||
@@ -41,6 +41,8 @@ addons:
|
||||
- le.wtf
|
||||
mariadb: "10.0"
|
||||
apt:
|
||||
sources:
|
||||
- augeas
|
||||
packages: # keep in sync with bootstrap/ubuntu.sh and Boulder
|
||||
- python
|
||||
- python-dev
|
||||
|
||||
@@ -21,7 +21,8 @@ WORKDIR /opt/letsencrypt
|
||||
# If <dest> doesn't exist, it is created along with all missing
|
||||
# directories in its path.
|
||||
|
||||
COPY bootstrap/ubuntu.sh /opt/letsencrypt/src/
|
||||
|
||||
COPY bootstrap/ubuntu.sh /opt/letsencrypt/src/ubuntu.sh
|
||||
RUN /opt/letsencrypt/src/ubuntu.sh && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/* \
|
||||
|
||||
@@ -22,7 +22,7 @@ WORKDIR /opt/letsencrypt
|
||||
|
||||
# TODO: Install non-default Python versions for tox.
|
||||
# TODO: Install Apache/Nginx for plugin development.
|
||||
COPY bootstrap/ubuntu.sh /opt/letsencrypt/src/
|
||||
COPY bootstrap/ubuntu.sh /opt/letsencrypt/src/ubuntu.sh
|
||||
RUN /opt/letsencrypt/src/ubuntu.sh && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/* \
|
||||
|
||||
@@ -35,11 +35,11 @@ It's all automated:
|
||||
|
||||
All you need to do to sign a single domain is::
|
||||
|
||||
user@www:~$ sudo letsencrypt -d www.example.org auth
|
||||
user@www:~$ sudo letsencrypt -d www.example.org certonly
|
||||
|
||||
For multiple domains (SAN) use::
|
||||
|
||||
user@www:~$ sudo letsencrypt -d www.example.org -d example.org auth
|
||||
user@www:~$ sudo letsencrypt -d www.example.org -d example.org certonly
|
||||
|
||||
and if you have a compatible web server (Apache or Nginx), Let's Encrypt can
|
||||
not only get a new certificate, but also deploy it and configure your
|
||||
|
||||
@@ -187,7 +187,7 @@ class KeyAuthorizationChallenge(_TokenDVChallenge):
|
||||
key_authorization=self.key_authorization(account_key))
|
||||
|
||||
@abc.abstractmethod
|
||||
def validation(self, account_key):
|
||||
def validation(self, account_key, **kwargs):
|
||||
"""Generate validation for the challenge.
|
||||
|
||||
Subclasses must implement this method, but they are likely to
|
||||
@@ -201,7 +201,7 @@ class KeyAuthorizationChallenge(_TokenDVChallenge):
|
||||
"""
|
||||
raise NotImplementedError() # pragma: no cover
|
||||
|
||||
def response_and_validation(self, account_key):
|
||||
def response_and_validation(self, account_key, *args, **kwargs):
|
||||
"""Generate response and validation.
|
||||
|
||||
Convenience function that return results of `response` and
|
||||
@@ -211,7 +211,8 @@ class KeyAuthorizationChallenge(_TokenDVChallenge):
|
||||
:rtype: tuple
|
||||
|
||||
"""
|
||||
return (self.response(account_key), self.validation(account_key))
|
||||
return (self.response(account_key),
|
||||
self.validation(account_key, *args, **kwargs))
|
||||
|
||||
|
||||
@ChallengeResponse.register
|
||||
@@ -220,6 +221,12 @@ class HTTP01Response(KeyAuthorizationChallengeResponse):
|
||||
typ = "http-01"
|
||||
|
||||
PORT = 80
|
||||
"""Verification port as defined by the protocol.
|
||||
|
||||
You can override it (e.g. for testing) by passing ``port`` to
|
||||
`simple_verify`.
|
||||
|
||||
"""
|
||||
|
||||
def simple_verify(self, chall, domain, account_public_key, port=None):
|
||||
"""Simple verify.
|
||||
@@ -246,7 +253,7 @@ class HTTP01Response(KeyAuthorizationChallengeResponse):
|
||||
# request URI, if it's standard.
|
||||
if port is not None and port != self.PORT:
|
||||
logger.warning(
|
||||
"Using non-standard port for SimpleHTTP verification: %s", port)
|
||||
"Using non-standard port for http-01 verification: %s", port)
|
||||
domain += ":{0}".format(port)
|
||||
|
||||
uri = chall.uri(domain)
|
||||
@@ -308,7 +315,7 @@ class HTTP01(KeyAuthorizationChallenge):
|
||||
"""
|
||||
return "http://" + domain + self.path
|
||||
|
||||
def validation(self, account_key):
|
||||
def validation(self, account_key, **unused_kwargs):
|
||||
"""Generate validation.
|
||||
|
||||
:param JWK account_key:
|
||||
@@ -318,89 +325,50 @@ class HTTP01(KeyAuthorizationChallenge):
|
||||
return self.key_authorization(account_key)
|
||||
|
||||
|
||||
@Challenge.register # pylint: disable=too-many-ancestors
|
||||
class DVSNI(_TokenDVChallenge):
|
||||
"""ACME "dvsni" challenge.
|
||||
|
||||
:ivar bytes token: Random data, **not** base64-encoded.
|
||||
|
||||
"""
|
||||
typ = "dvsni"
|
||||
|
||||
PORT = 443
|
||||
"""Port to perform DVSNI challenge."""
|
||||
|
||||
def gen_response(self, account_key, alg=jose.RS256, **kwargs):
|
||||
"""Generate response.
|
||||
|
||||
:param .JWK account_key: Private account key.
|
||||
:rtype: .DVSNIResponse
|
||||
|
||||
"""
|
||||
return DVSNIResponse(validation=jose.JWS.sign(
|
||||
payload=self.json_dumps(sort_keys=True).encode('utf-8'),
|
||||
key=account_key, alg=alg, **kwargs))
|
||||
|
||||
|
||||
@ChallengeResponse.register
|
||||
class DVSNIResponse(ChallengeResponse):
|
||||
"""ACME "dvsni" challenge response.
|
||||
|
||||
:param bytes s: Random data, **not** base64-encoded.
|
||||
|
||||
"""
|
||||
typ = "dvsni"
|
||||
class TLSSNI01Response(KeyAuthorizationChallengeResponse):
|
||||
"""ACME tls-sni-01 challenge response."""
|
||||
typ = "tls-sni-01"
|
||||
|
||||
DOMAIN_SUFFIX = b".acme.invalid"
|
||||
"""Domain name suffix."""
|
||||
|
||||
PORT = DVSNI.PORT
|
||||
"""Port to perform DVSNI challenge."""
|
||||
PORT = 443
|
||||
"""Verification port as defined by the protocol.
|
||||
|
||||
validation = jose.Field("validation", decoder=jose.JWS.from_json)
|
||||
You can override it (e.g. for testing) by passing ``port`` to
|
||||
`simple_verify`.
|
||||
|
||||
"""
|
||||
|
||||
@property
|
||||
def z(self): # pylint: disable=invalid-name
|
||||
"""The ``z`` parameter.
|
||||
def z(self):
|
||||
"""``z`` value used for verification.
|
||||
|
||||
:rtype: bytes
|
||||
:rtype bytes:
|
||||
|
||||
"""
|
||||
# Instance of 'Field' has no 'signature' member
|
||||
# pylint: disable=no-member
|
||||
return hashlib.sha256(self.validation.signature.encode(
|
||||
"signature").encode("utf-8")).hexdigest().encode()
|
||||
return hashlib.sha256(
|
||||
self.key_authorization.encode("utf-8")).hexdigest().lower().encode()
|
||||
|
||||
@property
|
||||
def z_domain(self):
|
||||
"""Domain name for certificate subjectAltName.
|
||||
"""Domain name used for verification, generated from `z`.
|
||||
|
||||
:rtype: bytes
|
||||
:rtype bytes:
|
||||
|
||||
"""
|
||||
z = self.z # pylint: disable=invalid-name
|
||||
return z[:32] + b'.' + z[32:] + self.DOMAIN_SUFFIX
|
||||
|
||||
@property
|
||||
def chall(self):
|
||||
"""Get challenge encoded in the `validation` payload.
|
||||
|
||||
:rtype: challenges.DVSNI
|
||||
|
||||
"""
|
||||
# pylint: disable=no-member
|
||||
return DVSNI.json_loads(self.validation.payload.decode('utf-8'))
|
||||
return self.z[:32] + b'.' + self.z[32:] + self.DOMAIN_SUFFIX
|
||||
|
||||
def gen_cert(self, key=None, bits=2048):
|
||||
"""Generate DVSNI certificate.
|
||||
"""Generate tls-sni-01 certificate.
|
||||
|
||||
: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`
|
||||
:rtype: `tuple` of `OpenSSL.crypto.X509` and `OpenSSL.crypto.PKey`
|
||||
|
||||
"""
|
||||
if key is None:
|
||||
@@ -411,11 +379,12 @@ class DVSNIResponse(ChallengeResponse):
|
||||
'dummy', self.z_domain.decode()], force_san=True), key
|
||||
|
||||
def probe_cert(self, domain, **kwargs):
|
||||
"""Probe DVSNI challenge certificate.
|
||||
"""Probe tls-sni-01 challenge certificate.
|
||||
|
||||
:param unicode domain:
|
||||
|
||||
"""
|
||||
# TODO: domain is not necessary if host is provided
|
||||
if "host" not in kwargs:
|
||||
host = socket.gethostbyname(domain)
|
||||
logging.debug('%s resolved to %s', domain, host)
|
||||
@@ -428,7 +397,7 @@ class DVSNIResponse(ChallengeResponse):
|
||||
return crypto_util.probe_sni(**kwargs)
|
||||
|
||||
def verify_cert(self, cert):
|
||||
"""Verify DVSNI challenge certificate."""
|
||||
"""Verify tls-sni-01 challenge certificate."""
|
||||
# pylint: disable=protected-access
|
||||
sans = crypto_util._pyopenssl_cert_or_req_san(cert)
|
||||
logging.debug('Certificate %s. SANs: %s', cert.digest('sha1'), sans)
|
||||
@@ -439,14 +408,15 @@ class DVSNIResponse(ChallengeResponse):
|
||||
"""Simple verify.
|
||||
|
||||
Verify ``validation`` using ``account_public_key``, optionally
|
||||
probe DVSNI certificate and check using `verify_cert`.
|
||||
probe tls-sni-01 certificate and check using `verify_cert`.
|
||||
|
||||
:param .challenges.DVSNI chall: Corresponding challenge.
|
||||
:param .challenges.TLSSNI01 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 int port: Port used to probe the certificate.
|
||||
|
||||
|
||||
:returns: ``True`` iff client's control of the domain has been
|
||||
@@ -454,20 +424,8 @@ class DVSNIResponse(ChallengeResponse):
|
||||
:rtype: bool
|
||||
|
||||
"""
|
||||
# pylint: disable=no-member
|
||||
if not self.validation.verify(key=account_public_key):
|
||||
return False
|
||||
|
||||
# TODO: it's not checked that payload has exectly 2 fields!
|
||||
try:
|
||||
decoded_chall = self.chall
|
||||
except jose.DeserializationError as error:
|
||||
logger.debug(error, exc_info=True)
|
||||
return False
|
||||
|
||||
if decoded_chall.token != chall.token:
|
||||
logger.debug("Wrong token: expected %r, found %r",
|
||||
chall.token, decoded_chall.token)
|
||||
if not self.verify(chall, account_public_key):
|
||||
logger.debug("Verification of key authorization in response failed")
|
||||
return False
|
||||
|
||||
if cert is None:
|
||||
@@ -480,6 +438,29 @@ class DVSNIResponse(ChallengeResponse):
|
||||
return self.verify_cert(cert)
|
||||
|
||||
|
||||
@Challenge.register # pylint: disable=too-many-ancestors
|
||||
class TLSSNI01(KeyAuthorizationChallenge):
|
||||
"""ACME tls-sni-01 challenge."""
|
||||
response_cls = TLSSNI01Response
|
||||
typ = response_cls.typ
|
||||
|
||||
# boulder#962, ietf-wg-acme#22
|
||||
#n = jose.Field("n", encoder=int, decoder=int)
|
||||
|
||||
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'))
|
||||
|
||||
|
||||
@Challenge.register
|
||||
class RecoveryContact(ContinuityChallenge):
|
||||
"""ACME "recoveryContact" challenge.
|
||||
|
||||
@@ -186,14 +186,112 @@ class HTTP01Test(unittest.TestCase):
|
||||
self.msg.update(token=b'..').good_token)
|
||||
|
||||
|
||||
class DVSNITest(unittest.TestCase):
|
||||
class TLSSNI01ResponseTest(unittest.TestCase):
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
|
||||
def setUp(self):
|
||||
from acme.challenges import DVSNI
|
||||
self.msg = DVSNI(
|
||||
from acme.challenges import TLSSNI01
|
||||
self.chall = TLSSNI01(
|
||||
token=jose.b64decode(b'a82d5ff8ef740d12881f6d3c2277ab2e'))
|
||||
|
||||
self.response = self.chall.response(KEY)
|
||||
self.jmsg = {
|
||||
'resource': 'challenge',
|
||||
'type': 'tls-sni-01',
|
||||
'keyAuthorization': self.response.key_authorization,
|
||||
}
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
label1 = b'dc38d9c3fa1a4fdcc3a5501f2d38583f'
|
||||
label2 = b'b7793728f084394f2a1afd459556bb5c'
|
||||
self.z = label1 + label2
|
||||
self.z_domain = label1 + b'.' + label2 + b'.acme.invalid'
|
||||
self.domain = 'foo.com'
|
||||
|
||||
def test_z_and_domain(self):
|
||||
self.assertEqual(self.z, self.response.z)
|
||||
self.assertEqual(self.z_domain, self.response.z_domain)
|
||||
|
||||
def test_to_partial_json(self):
|
||||
self.assertEqual(self.jmsg, self.response.to_partial_json())
|
||||
|
||||
def test_from_json(self):
|
||||
from acme.challenges import TLSSNI01Response
|
||||
self.assertEqual(self.response, TLSSNI01Response.from_json(self.jmsg))
|
||||
|
||||
def test_from_json_hashable(self):
|
||||
from acme.challenges import TLSSNI01Response
|
||||
hash(TLSSNI01Response.from_json(self.jmsg))
|
||||
|
||||
@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=self.z_domain)
|
||||
|
||||
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=mock.ANY)
|
||||
|
||||
self.response.probe_cert('foo.com', port=1234)
|
||||
mock_probe_sni.assert_called_with(
|
||||
host=mock.ANY, port=1234, name=mock.ANY)
|
||||
|
||||
self.response.probe_cert('foo.com', bar='baz')
|
||||
mock_probe_sni.assert_called_with(
|
||||
host=mock.ANY, port=mock.ANY, name=mock.ANY, bar='baz')
|
||||
|
||||
self.response.probe_cert('foo.com', name=b'xxx')
|
||||
mock_probe_sni.assert_called_with(
|
||||
host=mock.ANY, port=mock.ANY,
|
||||
name=self.z_domain)
|
||||
|
||||
def test_gen_verify_cert(self):
|
||||
key1 = test_util.load_pyopenssl_private_key('rsa512_key.pem')
|
||||
cert, key2 = self.response.gen_cert(key1)
|
||||
self.assertEqual(key1, key2)
|
||||
self.assertTrue(self.response.verify_cert(cert))
|
||||
|
||||
def test_gen_verify_cert_gen_key(self):
|
||||
cert, key = self.response.gen_cert()
|
||||
self.assertTrue(isinstance(key, OpenSSL.crypto.PKey))
|
||||
self.assertTrue(self.response.verify_cert(cert))
|
||||
|
||||
def test_verify_bad_cert(self):
|
||||
self.assertFalse(self.response.verify_cert(
|
||||
test_util.load_cert('cert.pem')))
|
||||
|
||||
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.TLSSNI01Response.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)
|
||||
|
||||
@mock.patch('acme.challenges.TLSSNI01Response.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 TLSSNI01Test(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
from acme.challenges import TLSSNI01
|
||||
self.msg = TLSSNI01(
|
||||
token=jose.b64decode('a82d5ff8ef740d12881f6d3c2277ab2e'))
|
||||
self.jmsg = {
|
||||
'type': 'dvsni',
|
||||
'type': 'tls-sni-01',
|
||||
'token': 'a82d5ff8ef740d12881f6d3c2277ab2e',
|
||||
}
|
||||
|
||||
@@ -201,144 +299,25 @@ class DVSNITest(unittest.TestCase):
|
||||
self.assertEqual(self.jmsg, self.msg.to_partial_json())
|
||||
|
||||
def test_from_json(self):
|
||||
from acme.challenges import DVSNI
|
||||
self.assertEqual(self.msg, DVSNI.from_json(self.jmsg))
|
||||
from acme.challenges import TLSSNI01
|
||||
self.assertEqual(self.msg, TLSSNI01.from_json(self.jmsg))
|
||||
|
||||
def test_from_json_hashable(self):
|
||||
from acme.challenges import DVSNI
|
||||
hash(DVSNI.from_json(self.jmsg))
|
||||
from acme.challenges import TLSSNI01
|
||||
hash(TLSSNI01.from_json(self.jmsg))
|
||||
|
||||
def test_from_json_invalid_token_length(self):
|
||||
from acme.challenges import DVSNI
|
||||
from acme.challenges import TLSSNI01
|
||||
self.jmsg['token'] = jose.encode_b64jose(b'abcd')
|
||||
self.assertRaises(
|
||||
jose.DeserializationError, DVSNI.from_json, self.jmsg)
|
||||
jose.DeserializationError, TLSSNI01.from_json, self.jmsg)
|
||||
|
||||
def test_gen_response(self):
|
||||
from acme.challenges import DVSNI
|
||||
self.assertEqual(self.msg, DVSNI.json_loads(
|
||||
self.msg.gen_response(KEY).validation.payload.decode()))
|
||||
|
||||
|
||||
class DVSNIResponseTest(unittest.TestCase):
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
|
||||
def setUp(self):
|
||||
from acme.challenges import DVSNI
|
||||
self.chall = DVSNI(
|
||||
token=jose.b64decode(b'a82d5ff8ef740d12881f6d3c2277ab2e'))
|
||||
|
||||
from acme.challenges import DVSNIResponse
|
||||
self.validation = jose.JWS.sign(
|
||||
payload=self.chall.json_dumps(sort_keys=True).encode(),
|
||||
key=KEY, alg=jose.RS256)
|
||||
self.msg = DVSNIResponse(validation=self.validation)
|
||||
self.jmsg_to = {
|
||||
'resource': 'challenge',
|
||||
'type': 'dvsni',
|
||||
'validation': self.validation,
|
||||
}
|
||||
self.jmsg_from = {
|
||||
'resource': 'challenge',
|
||||
'type': 'dvsni',
|
||||
'validation': self.validation.to_json(),
|
||||
}
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
label1 = b'e2df3498860637c667fedadc5a8494ec'
|
||||
label2 = b'09dcc75553c9b3bd73662b50e71b1e42'
|
||||
self.z = label1 + label2
|
||||
self.z_domain = label1 + b'.' + label2 + b'.acme.invalid'
|
||||
self.domain = 'foo.com'
|
||||
|
||||
def test_z_and_domain(self):
|
||||
self.assertEqual(self.z, self.msg.z)
|
||||
self.assertEqual(self.z_domain, self.msg.z_domain)
|
||||
|
||||
def test_to_partial_json(self):
|
||||
self.assertEqual(self.jmsg_to, self.msg.to_partial_json())
|
||||
|
||||
def test_from_json(self):
|
||||
from acme.challenges import DVSNIResponse
|
||||
self.assertEqual(self.msg, DVSNIResponse.from_json(self.jmsg_from))
|
||||
|
||||
def test_from_json_hashable(self):
|
||||
from acme.challenges import DVSNIResponse
|
||||
hash(DVSNIResponse.from_json(self.jmsg_from))
|
||||
|
||||
@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.msg.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.msg.PORT,
|
||||
name=self.z_domain)
|
||||
|
||||
self.msg.probe_cert('foo.com', host='8.8.8.8')
|
||||
mock_probe_sni.assert_called_with(
|
||||
host='8.8.8.8', port=mock.ANY, name=mock.ANY)
|
||||
|
||||
self.msg.probe_cert('foo.com', port=1234)
|
||||
mock_probe_sni.assert_called_with(
|
||||
host=mock.ANY, port=1234, name=mock.ANY)
|
||||
|
||||
self.msg.probe_cert('foo.com', bar='baz')
|
||||
mock_probe_sni.assert_called_with(
|
||||
host=mock.ANY, port=mock.ANY, name=mock.ANY, bar='baz')
|
||||
|
||||
self.msg.probe_cert('foo.com', name=b'xxx')
|
||||
mock_probe_sni.assert_called_with(
|
||||
host=mock.ANY, port=mock.ANY,
|
||||
name=self.z_domain)
|
||||
|
||||
def test_gen_verify_cert(self):
|
||||
key1 = test_util.load_pyopenssl_private_key('rsa512_key.pem')
|
||||
cert, key2 = self.msg.gen_cert(key1)
|
||||
self.assertEqual(key1, key2)
|
||||
self.assertTrue(self.msg.verify_cert(cert))
|
||||
|
||||
def test_gen_verify_cert_gen_key(self):
|
||||
cert, key = self.msg.gen_cert()
|
||||
self.assertTrue(isinstance(key, OpenSSL.crypto.PKey))
|
||||
self.assertTrue(self.msg.verify_cert(cert))
|
||||
|
||||
def test_verify_bad_cert(self):
|
||||
self.assertFalse(self.msg.verify_cert(test_util.load_cert('cert.pem')))
|
||||
|
||||
def test_simple_verify_wrong_account_key(self):
|
||||
self.assertFalse(self.msg.simple_verify(
|
||||
self.chall, self.domain, jose.JWKRSA.load(
|
||||
test_util.load_vector('rsa256_key.pem')).public_key()))
|
||||
|
||||
def test_simple_verify_wrong_payload(self):
|
||||
for payload in b'', b'{}':
|
||||
msg = self.msg.update(validation=jose.JWS.sign(
|
||||
payload=payload, key=KEY, alg=jose.RS256))
|
||||
self.assertFalse(msg.simple_verify(
|
||||
self.chall, self.domain, KEY.public_key()))
|
||||
|
||||
def test_simple_verify_wrong_token(self):
|
||||
msg = self.msg.update(validation=jose.JWS.sign(
|
||||
payload=self.chall.update(token=(b'b' * 20)).json_dumps().encode(),
|
||||
key=KEY, alg=jose.RS256))
|
||||
self.assertFalse(msg.simple_verify(
|
||||
self.chall, self.domain, KEY.public_key()))
|
||||
|
||||
@mock.patch('acme.challenges.DVSNIResponse.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.msg.simple_verify(
|
||||
self.chall, self.domain, KEY.public_key(),
|
||||
cert=mock.sentinel.cert))
|
||||
mock_verify_cert.assert_called_once_with(self.msg, mock.sentinel.cert)
|
||||
|
||||
@mock.patch('acme.challenges.DVSNIResponse.probe_cert')
|
||||
def test_simple_verify_false_on_probe_error(self, mock_probe_cert):
|
||||
mock_probe_cert.side_effect = errors.Error
|
||||
self.assertFalse(self.msg.simple_verify(
|
||||
self.chall, self.domain, KEY.public_key()))
|
||||
@mock.patch('acme.challenges.TLSSNI01Response.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)
|
||||
|
||||
|
||||
class RecoveryContactTest(unittest.TestCase):
|
||||
@@ -571,8 +550,6 @@ class ProofOfPossessionResponseTest(unittest.TestCase):
|
||||
class DNSTest(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.account_key = jose.JWKRSA.load(
|
||||
test_util.load_vector('rsa512_key.pem'))
|
||||
from acme.challenges import DNS
|
||||
self.msg = DNS(token=jose.b64decode(
|
||||
b'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA'))
|
||||
@@ -594,34 +571,33 @@ class DNSTest(unittest.TestCase):
|
||||
|
||||
def test_gen_check_validation(self):
|
||||
self.assertTrue(self.msg.check_validation(
|
||||
self.msg.gen_validation(self.account_key),
|
||||
self.account_key.public_key()))
|
||||
self.msg.gen_validation(KEY), KEY.public_key()))
|
||||
|
||||
def test_gen_check_validation_wrong_key(self):
|
||||
key2 = jose.JWKRSA.load(test_util.load_vector('rsa1024_key.pem'))
|
||||
self.assertFalse(self.msg.check_validation(
|
||||
self.msg.gen_validation(self.account_key), key2.public_key()))
|
||||
self.msg.gen_validation(KEY), key2.public_key()))
|
||||
|
||||
def test_check_validation_wrong_payload(self):
|
||||
validations = tuple(
|
||||
jose.JWS.sign(payload=payload, alg=jose.RS256, key=self.account_key)
|
||||
jose.JWS.sign(payload=payload, alg=jose.RS256, key=KEY)
|
||||
for payload in (b'', b'{}')
|
||||
)
|
||||
for validation in validations:
|
||||
self.assertFalse(self.msg.check_validation(
|
||||
validation, self.account_key.public_key()))
|
||||
validation, KEY.public_key()))
|
||||
|
||||
def test_check_validation_wrong_fields(self):
|
||||
bad_validation = jose.JWS.sign(
|
||||
payload=self.msg.update(token=b'x' * 20).json_dumps().encode('utf-8'),
|
||||
alg=jose.RS256, key=self.account_key)
|
||||
alg=jose.RS256, key=KEY)
|
||||
self.assertFalse(self.msg.check_validation(
|
||||
bad_validation, self.account_key.public_key()))
|
||||
bad_validation, KEY.public_key()))
|
||||
|
||||
def test_gen_response(self):
|
||||
with mock.patch('acme.challenges.DNS.gen_validation') as mock_gen:
|
||||
mock_gen.return_value = mock.sentinel.validation
|
||||
response = self.msg.gen_response(self.account_key)
|
||||
response = self.msg.gen_response(KEY)
|
||||
from acme.challenges import DNSResponse
|
||||
self.assertTrue(isinstance(response, DNSResponse))
|
||||
self.assertEqual(response.validation, mock.sentinel.validation)
|
||||
|
||||
@@ -481,11 +481,13 @@ class ClientNetwork(object):
|
||||
JSON_ERROR_CONTENT_TYPE = 'application/problem+json'
|
||||
REPLAY_NONCE_HEADER = 'Replay-Nonce'
|
||||
|
||||
def __init__(self, key, alg=jose.RS256, verify_ssl=True):
|
||||
def __init__(self, key, alg=jose.RS256, verify_ssl=True,
|
||||
user_agent='acme-python'):
|
||||
self.key = key
|
||||
self.alg = alg
|
||||
self.verify_ssl = verify_ssl
|
||||
self._nonces = set()
|
||||
self.user_agent = user_agent
|
||||
|
||||
def _wrap_in_jws(self, obj, nonce):
|
||||
"""Wrap `JSONDeSerializable` object in JWS.
|
||||
@@ -578,6 +580,8 @@ class ClientNetwork(object):
|
||||
logging.debug('Sending %s request to %s. args: %r, kwargs: %r',
|
||||
method, url, args, kwargs)
|
||||
kwargs['verify'] = self.verify_ssl
|
||||
kwargs.setdefault('headers', {})
|
||||
kwargs['headers'].setdefault('User-Agent', self.user_agent)
|
||||
response = requests.request(method, url, *args, **kwargs)
|
||||
logging.debug('Received %s. Headers: %s. Content: %r',
|
||||
response, response.headers, response.content)
|
||||
|
||||
@@ -396,7 +396,8 @@ class ClientNetworkTest(unittest.TestCase):
|
||||
|
||||
from acme.client import ClientNetwork
|
||||
self.net = ClientNetwork(
|
||||
key=KEY, alg=jose.RS256, verify_ssl=self.verify_ssl)
|
||||
key=KEY, alg=jose.RS256, verify_ssl=self.verify_ssl,
|
||||
user_agent='acme-python-test')
|
||||
|
||||
self.response = mock.MagicMock(ok=True, status_code=http_client.OK)
|
||||
self.response.headers = {}
|
||||
@@ -479,7 +480,7 @@ class ClientNetworkTest(unittest.TestCase):
|
||||
self.assertEqual(self.response, self.net._send_request(
|
||||
'HEAD', 'url', 'foo', bar='baz'))
|
||||
mock_requests.request.assert_called_once_with(
|
||||
'HEAD', 'url', 'foo', verify=mock.ANY, bar='baz')
|
||||
'HEAD', 'url', 'foo', verify=mock.ANY, bar='baz', headers=mock.ANY)
|
||||
|
||||
@mock.patch('acme.client.requests')
|
||||
def test_send_request_verify_ssl(self, mock_requests):
|
||||
@@ -492,7 +493,20 @@ class ClientNetworkTest(unittest.TestCase):
|
||||
self.assertEqual(
|
||||
self.response, self.net._send_request('GET', 'url'))
|
||||
mock_requests.request.assert_called_once_with(
|
||||
'GET', 'url', verify=verify)
|
||||
'GET', 'url', verify=verify, headers=mock.ANY)
|
||||
|
||||
@mock.patch('acme.client.requests')
|
||||
def test_send_request_user_agent(self, mock_requests):
|
||||
mock_requests.request.return_value = self.response
|
||||
# pylint: disable=protected-access
|
||||
self.net._send_request('GET', 'url', headers={'bar': 'baz'})
|
||||
mock_requests.request.assert_called_once_with(
|
||||
'GET', 'url', verify=mock.ANY,
|
||||
headers={'User-Agent': 'acme-python-test', 'bar': 'baz'})
|
||||
|
||||
self.net._send_request('GET', 'url', headers={'User-Agent': 'foo2'})
|
||||
mock_requests.request.assert_called_with(
|
||||
'GET', 'url', verify=mock.ANY, headers={'User-Agent': 'foo2'})
|
||||
|
||||
@mock.patch('acme.client.requests')
|
||||
def test_requests_error_passthrough(self, mock_requests):
|
||||
|
||||
@@ -13,7 +13,7 @@ from acme import errors
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# DVSNI certificate serving and probing is not affected by SSL
|
||||
# TLSSNI01 certificate serving and probing is not affected by SSL
|
||||
# vulnerabilities: prober needs to check certificate for expected
|
||||
# contents anyway. Working SNI is the only thing that's necessary for
|
||||
# the challenge and thus scoping down SSL/TLS method (version) would
|
||||
@@ -23,7 +23,7 @@ logger = logging.getLogger(__name__)
|
||||
# https://www.openssl.org/docs/ssl/SSLv23_method.html). _serve_sni
|
||||
# should be changed to use "set_options" to disable SSLv2 and SSLv3,
|
||||
# in case it's used for things other than probing/serving!
|
||||
_DEFAULT_DVSNI_SSL_METHOD = OpenSSL.SSL.SSLv23_METHOD
|
||||
_DEFAULT_TLSSNI01_SSL_METHOD = OpenSSL.SSL.SSLv23_METHOD
|
||||
|
||||
|
||||
class SSLSocket(object): # pylint: disable=too-few-public-methods
|
||||
@@ -35,7 +35,7 @@ class SSLSocket(object): # pylint: disable=too-few-public-methods
|
||||
:ivar method: See `OpenSSL.SSL.Context` for allowed values.
|
||||
|
||||
"""
|
||||
def __init__(self, sock, certs, method=_DEFAULT_DVSNI_SSL_METHOD):
|
||||
def __init__(self, sock, certs, method=_DEFAULT_TLSSNI01_SSL_METHOD):
|
||||
self.sock = sock
|
||||
self.certs = certs
|
||||
self.method = method
|
||||
@@ -103,7 +103,7 @@ class SSLSocket(object): # pylint: disable=too-few-public-methods
|
||||
|
||||
|
||||
def probe_sni(name, host, port=443, timeout=300,
|
||||
method=_DEFAULT_DVSNI_SSL_METHOD, source_address=('0', 0)):
|
||||
method=_DEFAULT_TLSSNI01_SSL_METHOD, source_address=('0', 0)):
|
||||
"""Probe SNI server for SSL certificate.
|
||||
|
||||
:param bytes name: Byte string to send as the server name in the
|
||||
|
||||
@@ -176,5 +176,5 @@ PS384 = JWASignature.register(_JWAPS('PS384', hashes.SHA384))
|
||||
PS512 = JWASignature.register(_JWAPS('PS512', hashes.SHA512))
|
||||
|
||||
ES256 = JWASignature.register(_JWAES('ES256'))
|
||||
ES256 = JWASignature.register(_JWAES('ES384'))
|
||||
ES256 = JWASignature.register(_JWAES('ES512'))
|
||||
ES384 = JWASignature.register(_JWAES('ES384'))
|
||||
ES512 = JWASignature.register(_JWAES('ES512'))
|
||||
|
||||
@@ -6,7 +6,6 @@ import logging
|
||||
import os
|
||||
import sys
|
||||
|
||||
import six
|
||||
from six.moves import BaseHTTPServer # pylint: disable=import-error
|
||||
from six.moves import http_client # pylint: disable=import-error
|
||||
from six.moves import socketserver # pylint: disable=import-error
|
||||
@@ -30,7 +29,7 @@ class TLSServer(socketserver.TCPServer):
|
||||
self.certs = kwargs.pop("certs", {})
|
||||
self.method = kwargs.pop(
|
||||
# pylint: disable=protected-access
|
||||
"method", crypto_util._DEFAULT_DVSNI_SSL_METHOD)
|
||||
"method", crypto_util._DEFAULT_TLSSNI01_SSL_METHOD)
|
||||
self.allow_reuse_address = kwargs.pop("allow_reuse_address", True)
|
||||
socketserver.TCPServer.__init__(self, *args, **kwargs)
|
||||
|
||||
@@ -50,12 +49,25 @@ class ACMEServerMixin: # pylint: disable=old-style-class
|
||||
allow_reuse_address = True
|
||||
|
||||
|
||||
class DVSNIServer(TLSServer, ACMEServerMixin):
|
||||
"""DVSNI Server."""
|
||||
class TLSSNI01Server(TLSServer, ACMEServerMixin):
|
||||
"""TLSSNI01 Server."""
|
||||
|
||||
def __init__(self, server_address, certs):
|
||||
TLSServer.__init__(
|
||||
self, server_address, socketserver.BaseRequestHandler, certs=certs)
|
||||
self, server_address, BaseRequestHandlerWithLogging, certs=certs)
|
||||
|
||||
|
||||
class BaseRequestHandlerWithLogging(socketserver.BaseRequestHandler):
|
||||
"""BaseRequestHandler with logging."""
|
||||
|
||||
def log_message(self, format, *args): # pylint: disable=redefined-builtin
|
||||
"""Log arbitrary message."""
|
||||
logger.debug("%s - - %s", self.client_address[0], format % args)
|
||||
|
||||
def handle(self):
|
||||
"""Handle request."""
|
||||
self.log_message("Incoming request")
|
||||
socketserver.BaseRequestHandler.handle(self)
|
||||
|
||||
|
||||
class HTTP01Server(BaseHTTPServer.HTTPServer, ACMEServerMixin):
|
||||
@@ -83,6 +95,15 @@ class HTTP01RequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
|
||||
self.simple_http_resources = kwargs.pop("simple_http_resources", set())
|
||||
socketserver.BaseRequestHandler.__init__(self, *args, **kwargs)
|
||||
|
||||
def log_message(self, format, *args): # pylint: disable=redefined-builtin
|
||||
"""Log arbitrary message."""
|
||||
logger.debug("%s - - %s", self.client_address[0], format % args)
|
||||
|
||||
def handle(self):
|
||||
"""Handle request."""
|
||||
self.log_message("Incoming request")
|
||||
BaseHTTPServer.BaseHTTPRequestHandler.handle(self)
|
||||
|
||||
def do_GET(self): # pylint: disable=invalid-name,missing-docstring
|
||||
if self.path == "/":
|
||||
self.handle_index()
|
||||
@@ -109,17 +130,17 @@ class HTTP01RequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
|
||||
"""Handle HTTP01 provisioned resources."""
|
||||
for resource in self.simple_http_resources:
|
||||
if resource.chall.path == self.path:
|
||||
logger.debug("Serving HTTP01 with token %r",
|
||||
resource.chall.encode("token"))
|
||||
self.log_message("Serving HTTP01 with token %r",
|
||||
resource.chall.encode("token"))
|
||||
self.send_response(http_client.OK)
|
||||
self.send_header("Content-type", resource.chall.CONTENT_TYPE)
|
||||
self.end_headers()
|
||||
self.wfile.write(resource.validation.encode())
|
||||
return
|
||||
else: # pylint: disable=useless-else-on-loop
|
||||
logger.debug("No resources to serve")
|
||||
logger.debug("%s does not correspond to any resource. ignoring",
|
||||
self.path)
|
||||
self.log_message("No resources to serve")
|
||||
self.log_message("%s does not correspond to any resource. ignoring",
|
||||
self.path)
|
||||
|
||||
@classmethod
|
||||
def partial_init(cls, simple_http_resources):
|
||||
@@ -134,8 +155,8 @@ class HTTP01RequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
|
||||
cls, simple_http_resources=simple_http_resources)
|
||||
|
||||
|
||||
def simple_dvsni_server(cli_args, forever=True):
|
||||
"""Run simple standalone DVSNI server."""
|
||||
def simple_tls_sni_01_server(cli_args, forever=True):
|
||||
"""Run simple standalone TLSSNI01 server."""
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
@@ -158,9 +179,8 @@ def simple_dvsni_server(cli_args, forever=True):
|
||||
OpenSSL.crypto.load_certificate(
|
||||
OpenSSL.crypto.FILETYPE_PEM, cert_contents))
|
||||
|
||||
server = DVSNIServer(('', int(args.port)), certs=certs)
|
||||
six.print_("Serving at https://localhost:{0}...".format(
|
||||
server.socket.getsockname()[1]))
|
||||
server = TLSSNI01Server(('', int(args.port)), certs=certs)
|
||||
logger.info("Serving at https://%s:%s...", *server.socket.getsockname()[:2])
|
||||
if forever: # pragma: no cover
|
||||
server.serve_forever()
|
||||
else:
|
||||
@@ -168,4 +188,4 @@ def simple_dvsni_server(cli_args, forever=True):
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(simple_dvsni_server(sys.argv)) # pragma: no cover
|
||||
sys.exit(simple_tls_sni_01_server(sys.argv)) # pragma: no cover
|
||||
|
||||
@@ -28,8 +28,8 @@ class TLSServerTest(unittest.TestCase):
|
||||
server.server_close() # pylint: disable=no-member
|
||||
|
||||
|
||||
class DVSNIServerTest(unittest.TestCase):
|
||||
"""Test for acme.standalone.DVSNIServer."""
|
||||
class TLSSNI01ServerTest(unittest.TestCase):
|
||||
"""Test for acme.standalone.TLSSNI01Server."""
|
||||
|
||||
def setUp(self):
|
||||
self.certs = {
|
||||
@@ -37,8 +37,8 @@ class DVSNIServerTest(unittest.TestCase):
|
||||
# pylint: disable=protected-access
|
||||
test_util.load_cert('cert.pem')._wrapped),
|
||||
}
|
||||
from acme.standalone import DVSNIServer
|
||||
self.server = DVSNIServer(("", 0), certs=self.certs)
|
||||
from acme.standalone import TLSSNI01Server
|
||||
self.server = TLSSNI01Server(("", 0), certs=self.certs)
|
||||
# pylint: disable=no-member
|
||||
self.thread = threading.Thread(target=self.server.serve_forever)
|
||||
self.thread.start()
|
||||
@@ -106,8 +106,8 @@ class HTTP01ServerTest(unittest.TestCase):
|
||||
self.assertFalse(self._test_http01(add=False))
|
||||
|
||||
|
||||
class TestSimpleDVSNIServer(unittest.TestCase):
|
||||
"""Tests for acme.standalone.simple_dvsni_server."""
|
||||
class TestSimpleTLSSNI01Server(unittest.TestCase):
|
||||
"""Tests for acme.standalone.simple_tls_sni_01_server."""
|
||||
|
||||
def setUp(self):
|
||||
# mirror ../examples/standalone
|
||||
@@ -118,12 +118,14 @@ class TestSimpleDVSNIServer(unittest.TestCase):
|
||||
shutil.copy(test_util.vector_path('rsa512_key.pem'),
|
||||
os.path.join(localhost_dir, 'key.pem'))
|
||||
|
||||
from acme.standalone import simple_dvsni_server
|
||||
from acme.standalone import simple_tls_sni_01_server
|
||||
self.port = 1234
|
||||
self.thread = threading.Thread(target=simple_dvsni_server, kwargs={
|
||||
'cli_args': ('xxx', '--port', str(self.port)),
|
||||
'forever': False,
|
||||
})
|
||||
self.thread = threading.Thread(
|
||||
target=simple_tls_sni_01_server, kwargs={
|
||||
'cli_args': ('xxx', '--port', str(self.port)),
|
||||
'forever': False,
|
||||
},
|
||||
)
|
||||
self.old_cwd = os.getcwd()
|
||||
os.chdir(self.test_cwd)
|
||||
self.thread.start()
|
||||
|
||||
@@ -227,25 +227,25 @@ htmlhelp_basename = 'acme-pythondoc'
|
||||
# -- Options for LaTeX output ---------------------------------------------
|
||||
|
||||
latex_elements = {
|
||||
# The paper size ('letterpaper' or 'a4paper').
|
||||
#'papersize': 'letterpaper',
|
||||
# The paper size ('letterpaper' or 'a4paper').
|
||||
#'papersize': 'letterpaper',
|
||||
|
||||
# The font size ('10pt', '11pt' or '12pt').
|
||||
#'pointsize': '10pt',
|
||||
# The font size ('10pt', '11pt' or '12pt').
|
||||
#'pointsize': '10pt',
|
||||
|
||||
# Additional stuff for the LaTeX preamble.
|
||||
#'preamble': '',
|
||||
# Additional stuff for the LaTeX preamble.
|
||||
#'preamble': '',
|
||||
|
||||
# Latex figure (float) alignment
|
||||
#'figure_align': 'htbp',
|
||||
# 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, 'acme-python.tex', u'acme-python Documentation',
|
||||
u'Let\'s Encrypt Project', 'manual'),
|
||||
(master_doc, 'acme-python.tex', u'acme-python Documentation',
|
||||
u'Let\'s Encrypt Project', 'manual'),
|
||||
]
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top of
|
||||
@@ -289,9 +289,9 @@ man_pages = [
|
||||
# (source start file, target name, title, author,
|
||||
# dir menu entry, description, category)
|
||||
texinfo_documents = [
|
||||
(master_doc, 'acme-python', u'acme-python Documentation',
|
||||
author, 'acme-python', 'One line description of project.',
|
||||
'Miscellaneous'),
|
||||
(master_doc, 'acme-python', u'acme-python Documentation',
|
||||
author, 'acme-python', 'One line description of project.',
|
||||
'Miscellaneous'),
|
||||
]
|
||||
|
||||
# Documents to append as an appendix to all manuals.
|
||||
|
||||
@@ -44,7 +44,7 @@ apt-get install -y --no-install-recommends \
|
||||
libffi-dev \
|
||||
ca-certificates \
|
||||
|
||||
if ! which virtualenv > /dev/null ; then
|
||||
if ! command -v virtualenv > /dev/null ; then
|
||||
echo Failed to install a working \"virtualenv\" command, exiting
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -16,10 +16,12 @@ else
|
||||
fi
|
||||
|
||||
# "git-core" seems to be an alias for "git" in CentOS 7 (yum search fails)
|
||||
# Amazon Linux 2015.03 needs python27-virtualenv rather than python-virtualenv
|
||||
$tool install -y \
|
||||
git-core \
|
||||
python \
|
||||
python-devel \
|
||||
python27-virtualenv \
|
||||
python-virtualenv \
|
||||
gcc \
|
||||
dialog \
|
||||
|
||||
14
bootstrap/_suse_common.sh
Executable file
14
bootstrap/_suse_common.sh
Executable file
@@ -0,0 +1,14 @@
|
||||
#!/bin/sh
|
||||
|
||||
# SLE12 dont have python-virtualenv
|
||||
|
||||
zypper -nq in -l git-core \
|
||||
python \
|
||||
python-devel \
|
||||
python-virtualenv \
|
||||
gcc \
|
||||
dialog \
|
||||
augeas-lenses \
|
||||
libopenssl-devel \
|
||||
libffi-devel \
|
||||
ca-certificates \
|
||||
@@ -29,6 +29,9 @@ elif [ -f /etc/gentoo-release ] ; then
|
||||
elif uname | grep -iq FreeBSD ; then
|
||||
echo "Bootstrapping dependencies for FreeBSD..."
|
||||
$SUDO $BOOTSTRAP/freebsd.sh
|
||||
elif `grep -qs openSUSE /etc/os-release` ; then
|
||||
echo "Bootstrapping dependencies for openSUSE.."
|
||||
$SUDO $BOOTSTRAP/suse.sh
|
||||
elif uname | grep -iq Darwin ; then
|
||||
echo "Bootstrapping dependencies for Mac OS X..."
|
||||
echo "WARNING: Mac support is very experimental at present..."
|
||||
|
||||
1
bootstrap/suse.sh
Symbolic link
1
bootstrap/suse.sh
Symbolic link
@@ -0,0 +1 @@
|
||||
_suse_common.sh
|
||||
26
docs/conf.py
26
docs/conf.py
@@ -230,25 +230,25 @@ htmlhelp_basename = 'LetsEncryptdoc'
|
||||
# -- Options for LaTeX output ---------------------------------------------
|
||||
|
||||
latex_elements = {
|
||||
# The paper size ('letterpaper' or 'a4paper').
|
||||
#'papersize': 'letterpaper',
|
||||
# The paper size ('letterpaper' or 'a4paper').
|
||||
#'papersize': 'letterpaper',
|
||||
|
||||
# The font size ('10pt', '11pt' or '12pt').
|
||||
#'pointsize': '10pt',
|
||||
# The font size ('10pt', '11pt' or '12pt').
|
||||
#'pointsize': '10pt',
|
||||
|
||||
# Additional stuff for the LaTeX preamble.
|
||||
#'preamble': '',
|
||||
# Additional stuff for the LaTeX preamble.
|
||||
#'preamble': '',
|
||||
|
||||
# Latex figure (float) alignment
|
||||
#'figure_align': 'htbp',
|
||||
# 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 = [
|
||||
('index', 'LetsEncrypt.tex', u'Let\'s Encrypt Documentation',
|
||||
u'Let\'s Encrypt Project', 'manual'),
|
||||
('index', 'LetsEncrypt.tex', u'Let\'s Encrypt Documentation',
|
||||
u'Let\'s Encrypt Project', 'manual'),
|
||||
]
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top of
|
||||
@@ -295,9 +295,9 @@ man_pages = [
|
||||
# (source start file, target name, title, author,
|
||||
# dir menu entry, description, category)
|
||||
texinfo_documents = [
|
||||
('index', 'LetsEncrypt', u'Let\'s Encrypt Documentation',
|
||||
u'Let\'s Encrypt Project', 'LetsEncrypt', 'One line description of project.',
|
||||
'Miscellaneous'),
|
||||
('index', 'LetsEncrypt', u'Let\'s Encrypt Documentation',
|
||||
u'Let\'s Encrypt Project', 'LetsEncrypt', 'One line description of project.',
|
||||
'Miscellaneous'),
|
||||
]
|
||||
|
||||
# Documents to append as an appendix to all manuals.
|
||||
|
||||
@@ -151,7 +151,7 @@ certificate for some domain name by solving challenges received from
|
||||
the ACME server. From the protocol, there are essentially two
|
||||
different types of challenges. Challenges that must be solved by
|
||||
individual plugins in order to satisfy domain validation (subclasses
|
||||
of `~.DVChallenge`, i.e. `~.challenges.DVSNI`,
|
||||
of `~.DVChallenge`, i.e. `~.challenges.TLSSNI01`,
|
||||
`~.challenges.HTTP01`, `~.challenges.DNS`) and continuity specific
|
||||
challenges (subclasses of `~.ContinuityChallenge`,
|
||||
i.e. `~.challenges.RecoveryToken`, `~.challenges.RecoveryContact`,
|
||||
@@ -160,7 +160,7 @@ always handled by the `~.ContinuityAuthenticator`, while plugins are
|
||||
expected to handle `~.DVChallenge` types.
|
||||
Right now, we have two authenticator plugins, the `~.ApacheConfigurator`
|
||||
and the `~.StandaloneAuthenticator`. The Standalone and Apache
|
||||
authenticators only solve the `~.challenges.DVSNI` challenge currently.
|
||||
authenticators only solve the `~.challenges.TLSSNI01` challenge currently.
|
||||
(You can set which challenges your authenticator can handle through the
|
||||
:meth:`~.IAuthenticator.get_chall_pref`.
|
||||
|
||||
|
||||
@@ -86,8 +86,15 @@ in ``/etc/letsencrypt/live`` on the host.
|
||||
.. _`install Docker`: https://docs.docker.com/userguide/
|
||||
|
||||
|
||||
Distro packages
|
||||
---------------
|
||||
Operating System Packages
|
||||
--------------------------
|
||||
|
||||
**FreeBSD**
|
||||
|
||||
* Port: ``cd /usr/ports/security/py-letsencrypt && make install clean``
|
||||
* Package: ``pkg install py27-letsencrypt``
|
||||
|
||||
**Other Operating Systems**
|
||||
|
||||
Unfortunately, this is an ongoing effort. If you'd like to package
|
||||
Let's Encrypt client for your distribution of choice please have a
|
||||
@@ -128,7 +135,7 @@ Plugin A I Notes and status
|
||||
========== = = ================================================================
|
||||
standalone Y N Very stable. Uses port 80 (force by
|
||||
``--standalone-supported-challenges http-01``) or 443
|
||||
(force by ``--standalone-supported-challenges dvsni``).
|
||||
(force by ``--standalone-supported-challenges tls-sni-01``).
|
||||
apache Y Y Alpha. Automates Apache installation, works fairly well but on
|
||||
Debian-based distributions only for now.
|
||||
webroot Y N Works with already running webserver, by writing necessary files
|
||||
@@ -197,7 +204,7 @@ The following files are available:
|
||||
|
||||
.. warning:: This **must be kept secret at all times**! Never share
|
||||
it with anyone, including Let's Encrypt developers. You cannot
|
||||
put it into safe, however - your server still needs to access
|
||||
put it into a safe, however - your server still needs to access
|
||||
this file in order for SSL/TLS to work.
|
||||
|
||||
This is what Apache needs for `SSLCertificateKeyFile
|
||||
|
||||
@@ -16,7 +16,7 @@ server = https://acme-staging.api.letsencrypt.org/directory
|
||||
|
||||
# Uncomment to use the standalone authenticator on port 443
|
||||
# authenticator = standalone
|
||||
# standalone-supported-challenges = dvsni
|
||||
# standalone-supported-challenges = tls-sni-01
|
||||
|
||||
# Uncomment to use the webroot authenticator. Replace webroot-path with the
|
||||
# path to the public_html / webroot folder being served by your web server.
|
||||
|
||||
@@ -232,25 +232,25 @@ htmlhelp_basename = 'letsencrypt-apachedoc'
|
||||
# -- Options for LaTeX output ---------------------------------------------
|
||||
|
||||
latex_elements = {
|
||||
# The paper size ('letterpaper' or 'a4paper').
|
||||
#'papersize': 'letterpaper',
|
||||
# The paper size ('letterpaper' or 'a4paper').
|
||||
#'papersize': 'letterpaper',
|
||||
|
||||
# The font size ('10pt', '11pt' or '12pt').
|
||||
#'pointsize': '10pt',
|
||||
# The font size ('10pt', '11pt' or '12pt').
|
||||
#'pointsize': '10pt',
|
||||
|
||||
# Additional stuff for the LaTeX preamble.
|
||||
#'preamble': '',
|
||||
# Additional stuff for the LaTeX preamble.
|
||||
#'preamble': '',
|
||||
|
||||
# Latex figure (float) alignment
|
||||
#'figure_align': 'htbp',
|
||||
# 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, 'letsencrypt-apache.tex', u'letsencrypt-apache Documentation',
|
||||
u'Let\'s Encrypt Project', 'manual'),
|
||||
(master_doc, 'letsencrypt-apache.tex', u'letsencrypt-apache Documentation',
|
||||
u'Let\'s Encrypt Project', 'manual'),
|
||||
]
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top of
|
||||
@@ -293,9 +293,9 @@ man_pages = [
|
||||
# (source start file, target name, title, author,
|
||||
# dir menu entry, description, category)
|
||||
texinfo_documents = [
|
||||
(master_doc, 'letsencrypt-apache', u'letsencrypt-apache Documentation',
|
||||
author, 'letsencrypt-apache', 'One line description of project.',
|
||||
'Miscellaneous'),
|
||||
(master_doc, 'letsencrypt-apache', u'letsencrypt-apache Documentation',
|
||||
author, 'letsencrypt-apache', 'One line description of project.',
|
||||
'Miscellaneous'),
|
||||
]
|
||||
|
||||
# Documents to append as an appendix to all manuals.
|
||||
|
||||
@@ -73,7 +73,8 @@ class AugeasConfigurator(common.Plugin):
|
||||
|
||||
This function first checks for save errors, if none are found,
|
||||
all configuration changes made will be saved. According to the
|
||||
function parameters.
|
||||
function parameters. If an exception is raised, a new checkpoint
|
||||
was not created.
|
||||
|
||||
: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
|
||||
@@ -82,8 +83,9 @@ class AugeasConfigurator(common.Plugin):
|
||||
:param bool temporary: Indicates whether the changes made will
|
||||
be quickly reversed in the future (ie. challenges)
|
||||
|
||||
:raises .errors.PluginError: If there was an error in Augeas, in an
|
||||
attempt to save the configuration, or an error creating a checkpoint
|
||||
:raises .errors.PluginError: If there was an error in Augeas, in
|
||||
an attempt to save the configuration, or an error creating a
|
||||
checkpoint
|
||||
|
||||
"""
|
||||
save_state = self.aug.get("/augeas/save")
|
||||
@@ -122,16 +124,16 @@ class AugeasConfigurator(common.Plugin):
|
||||
except errors.ReverterError as err:
|
||||
raise errors.PluginError(str(err))
|
||||
|
||||
self.aug.set("/augeas/save", save_state)
|
||||
self.save_notes = ""
|
||||
self.aug.save()
|
||||
|
||||
if title and not temporary:
|
||||
try:
|
||||
self.reverter.finalize_checkpoint(title)
|
||||
except errors.ReverterError as err:
|
||||
raise errors.PluginError(str(err))
|
||||
|
||||
self.aug.set("/augeas/save", save_state)
|
||||
self.save_notes = ""
|
||||
self.aug.save()
|
||||
|
||||
def _log_save_errors(self, ex_errs):
|
||||
"""Log errors due to bad Augeas save.
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@ import zope.interface
|
||||
|
||||
from acme import challenges
|
||||
|
||||
from letsencrypt import achallenges
|
||||
from letsencrypt import errors
|
||||
from letsencrypt import interfaces
|
||||
from letsencrypt import le_util
|
||||
@@ -163,7 +162,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
||||
# Get all of the available vhosts
|
||||
self.vhosts = self.get_virtual_hosts()
|
||||
|
||||
temp_install(self.mod_ssl_conf)
|
||||
install_ssl_options_conf(self.mod_ssl_conf)
|
||||
|
||||
def deploy_cert(self, domain, cert_path, key_path,
|
||||
chain_path=None, fullchain_path=None): # pylint: disable=unused-argument
|
||||
@@ -308,6 +307,8 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
||||
best_points = 0
|
||||
|
||||
for vhost in self.vhosts:
|
||||
if vhost.modmacro is True:
|
||||
continue
|
||||
if target_name in vhost.get_names():
|
||||
points = 2
|
||||
elif any(addr.get_addr() == target_name for addr in vhost.addrs):
|
||||
@@ -327,7 +328,10 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
||||
# No winners here... is there only one reasonable vhost?
|
||||
if best_candidate is None:
|
||||
# reasonable == Not all _default_ addrs
|
||||
reasonable_vhosts = self._non_default_vhosts()
|
||||
vhosts = self._non_default_vhosts()
|
||||
# remove mod_macro hosts from reasonable vhosts
|
||||
reasonable_vhosts = [vh for vh
|
||||
in vhosts if vh.modmacro is False]
|
||||
if len(reasonable_vhosts) == 1:
|
||||
best_candidate = reasonable_vhosts[0]
|
||||
|
||||
@@ -349,8 +353,12 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
||||
"""
|
||||
all_names = set()
|
||||
|
||||
vhost_macro = []
|
||||
|
||||
for vhost in self.vhosts:
|
||||
all_names.update(vhost.get_names())
|
||||
if vhost.modmacro:
|
||||
vhost_macro.append(vhost.filep)
|
||||
|
||||
for addr in vhost.addrs:
|
||||
if common.hostname_regex.match(addr.get_addr()):
|
||||
@@ -360,6 +368,12 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
||||
if name:
|
||||
all_names.add(name)
|
||||
|
||||
if len(vhost_macro) > 0:
|
||||
zope.component.getUtility(interfaces.IDisplay).notification(
|
||||
"Apache mod_macro seems to be in use in file(s):\n{0}"
|
||||
"\n\nUnfortunately mod_macro is not yet supported".format(
|
||||
"\n ".join(vhost_macro)))
|
||||
|
||||
return all_names
|
||||
|
||||
def get_name_from_ip(self, addr): # pylint: disable=no-self-use
|
||||
@@ -396,11 +410,15 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
||||
"ServerAlias", None, start=host.path, exclude=False)
|
||||
|
||||
for alias in serveralias_match:
|
||||
host.aliases.add(self.parser.get_arg(alias))
|
||||
serveralias = self.parser.get_arg(alias)
|
||||
if not host.modmacro:
|
||||
host.aliases.add(serveralias)
|
||||
|
||||
if servername_match:
|
||||
# Get last ServerName as each overwrites the previous
|
||||
host.name = self.parser.get_arg(servername_match[-1])
|
||||
servername = self.parser.get_arg(servername_match[-1])
|
||||
if not host.modmacro:
|
||||
host.name = servername
|
||||
|
||||
def _create_vhost(self, path):
|
||||
"""Used by get_virtual_hosts to create vhost objects
|
||||
@@ -423,7 +441,12 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
||||
filename = get_file_path(path)
|
||||
is_enabled = self.is_site_enabled(filename)
|
||||
|
||||
vhost = obj.VirtualHost(filename, path, addrs, is_ssl, is_enabled)
|
||||
macro = False
|
||||
if "/macro/" in path.lower():
|
||||
macro = True
|
||||
|
||||
vhost = obj.VirtualHost(filename, path, addrs, is_ssl,
|
||||
is_enabled, modmacro=macro)
|
||||
self._add_servernames(vhost)
|
||||
return vhost
|
||||
|
||||
@@ -1179,7 +1202,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
||||
###########################################################################
|
||||
def get_chall_pref(self, unused_domain): # pylint: disable=no-self-use
|
||||
"""Return list of challenge preferences."""
|
||||
return [challenges.DVSNI]
|
||||
return [challenges.TLSSNI01]
|
||||
|
||||
def perform(self, achalls):
|
||||
"""Perform the configuration related challenge.
|
||||
@@ -1194,11 +1217,10 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
||||
apache_dvsni = dvsni.ApacheDvsni(self)
|
||||
|
||||
for i, achall in enumerate(achalls):
|
||||
if isinstance(achall, achallenges.DVSNI):
|
||||
# Currently also have dvsni hold associated index
|
||||
# of the challenge. This helps to put all of the responses back
|
||||
# together when they are all complete.
|
||||
apache_dvsni.add_chall(achall, i)
|
||||
# Currently also have dvsni hold associated index
|
||||
# of the challenge. This helps to put all of the responses back
|
||||
# together when they are all complete.
|
||||
apache_dvsni.add_chall(achall, i)
|
||||
|
||||
sni_response = apache_dvsni.perform()
|
||||
if sni_response:
|
||||
@@ -1298,7 +1320,7 @@ def get_file_path(vhost_path):
|
||||
avail_fp = vhost_path[6:]
|
||||
# This can be optimized...
|
||||
while True:
|
||||
# Cast both to lowercase to be case insensitive
|
||||
# Cast all to lowercase to be case insensitive
|
||||
find_if = avail_fp.lower().find("/ifmodule")
|
||||
if find_if != -1:
|
||||
avail_fp = avail_fp[:find_if]
|
||||
@@ -1307,16 +1329,26 @@ def get_file_path(vhost_path):
|
||||
if find_vh != -1:
|
||||
avail_fp = avail_fp[:find_vh]
|
||||
continue
|
||||
find_macro = avail_fp.lower().find("/macro")
|
||||
if find_macro != -1:
|
||||
avail_fp = avail_fp[:find_macro]
|
||||
continue
|
||||
break
|
||||
return avail_fp
|
||||
|
||||
|
||||
def temp_install(options_ssl):
|
||||
"""Temporary install for convenience."""
|
||||
# WARNING: THIS IS A POTENTIAL SECURITY VULNERABILITY
|
||||
# THIS SHOULD BE HANDLED BY THE PACKAGE MANAGER
|
||||
# AND TAKEN OUT BEFORE RELEASE, INSTEAD
|
||||
# SHOWING A NICE ERROR MESSAGE ABOUT THE PROBLEM.
|
||||
def install_ssl_options_conf(options_ssl):
|
||||
"""
|
||||
Copy Let's Encrypt's SSL options file into the system's config dir if
|
||||
required.
|
||||
"""
|
||||
# XXX if we ever try to enforce a local privilege boundary (eg, running
|
||||
# letsencrypt for unprivileged users via setuid), this function will need
|
||||
# to be modified.
|
||||
|
||||
# XXX if the user is in security-autoupdate mode, we should be willing to
|
||||
# overwrite the options_ssl file at least if it's unmodified:
|
||||
# https://github.com/letsencrypt/letsencrypt/issues/1123
|
||||
|
||||
# Check to make sure options-ssl.conf is installed
|
||||
if not os.path.isfile(options_ssl):
|
||||
|
||||
@@ -7,14 +7,14 @@ from letsencrypt_apache import obj
|
||||
from letsencrypt_apache import parser
|
||||
|
||||
|
||||
class ApacheDvsni(common.Dvsni):
|
||||
class ApacheDvsni(common.TLSSNI01):
|
||||
"""Class performs DVSNI challenges within the Apache configurator.
|
||||
|
||||
:ivar configurator: ApacheConfigurator object
|
||||
:type configurator: :class:`~apache.configurator.ApacheConfigurator`
|
||||
|
||||
:ivar list achalls: Annotated :class:`~letsencrypt.achallenges.DVSNI`
|
||||
challenges.
|
||||
:ivar list achalls: Annotated tls-sni-01
|
||||
(`.KeyAuthorizationAnnotatedChallenge`) challenges.
|
||||
|
||||
:param list indices: Meant to hold indices of challenges in a
|
||||
larger array. ApacheDvsni is capable of solving many challenges
|
||||
@@ -62,7 +62,7 @@ class ApacheDvsni(common.Dvsni):
|
||||
|
||||
# Prepare the server for HTTPS
|
||||
self.configurator.prepare_server_https(
|
||||
str(self.configurator.config.dvsni_port), True)
|
||||
str(self.configurator.config.tls_sni_01_port), True)
|
||||
|
||||
responses = []
|
||||
|
||||
@@ -114,14 +114,15 @@ class ApacheDvsni(common.Dvsni):
|
||||
|
||||
# TODO: Checkout _default_ rules.
|
||||
dvsni_addrs = set()
|
||||
default_addr = obj.Addr(("*", str(self.configurator.config.dvsni_port)))
|
||||
default_addr = obj.Addr(("*", str(
|
||||
self.configurator.config.tls_sni_01_port)))
|
||||
|
||||
for addr in vhost.addrs:
|
||||
if "_default_" == addr.get_addr():
|
||||
dvsni_addrs.add(default_addr)
|
||||
else:
|
||||
dvsni_addrs.add(
|
||||
addr.get_sni_addr(self.configurator.config.dvsni_port))
|
||||
addr.get_sni_addr(self.configurator.config.tls_sni_01_port))
|
||||
|
||||
return dvsni_addrs
|
||||
|
||||
@@ -144,8 +145,8 @@ class ApacheDvsni(common.Dvsni):
|
||||
def _get_config_text(self, achall, ip_addrs):
|
||||
"""Chocolate virtual server configuration text
|
||||
|
||||
:param achall: Annotated DVSNI challenge.
|
||||
:type achall: :class:`letsencrypt.achallenges.DVSNI`
|
||||
:param .KeyAuthorizationAnnotatedChallenge achall: Annotated
|
||||
DVSNI challenge.
|
||||
|
||||
:param list ip_addrs: addresses of challenged domain
|
||||
:class:`list` of type `~.obj.Addr`
|
||||
@@ -164,7 +165,7 @@ class ApacheDvsni(common.Dvsni):
|
||||
# https://docs.python.org/2.7/reference/lexical_analysis.html
|
||||
return self.VHOST_TEMPLATE.format(
|
||||
vhost=ips,
|
||||
server_name=achall.gen_response(achall.account_key).z_domain,
|
||||
server_name=achall.response(achall.account_key).z_domain,
|
||||
ssl_options_conf_path=self.configurator.mod_ssl_conf,
|
||||
cert_path=self.get_cert_path(achall),
|
||||
key_path=self.get_key_path(achall),
|
||||
|
||||
@@ -102,6 +102,7 @@ class VirtualHost(object): # pylint: disable=too-few-public-methods
|
||||
|
||||
:ivar bool ssl: SSLEngine on in vhost
|
||||
:ivar bool enabled: Virtual host is enabled
|
||||
:ivar bool modmacro: VirtualHost is using mod_macro
|
||||
|
||||
https://httpd.apache.org/docs/2.4/vhosts/details.html
|
||||
|
||||
@@ -112,7 +113,9 @@ class VirtualHost(object): # pylint: disable=too-few-public-methods
|
||||
# ?: is used for not returning enclosed characters
|
||||
strip_name = re.compile(r"^(?:.+://)?([^ :$]*)")
|
||||
|
||||
def __init__(self, filep, path, addrs, ssl, enabled, name=None, aliases=None):
|
||||
def __init__(self, filep, path, addrs, ssl, enabled, name=None,
|
||||
aliases=None, modmacro=False):
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
"""Initialize a VH."""
|
||||
self.filep = filep
|
||||
@@ -122,6 +125,7 @@ class VirtualHost(object): # pylint: disable=too-few-public-methods
|
||||
self.aliases = aliases if aliases is not None else set()
|
||||
self.ssl = ssl
|
||||
self.enabled = enabled
|
||||
self.modmacro = modmacro
|
||||
|
||||
def get_names(self):
|
||||
"""Return a set of all names."""
|
||||
@@ -141,21 +145,25 @@ class VirtualHost(object): # pylint: disable=too-few-public-methods
|
||||
"Name: {name}\n"
|
||||
"Aliases: {aliases}\n"
|
||||
"TLS Enabled: {tls}\n"
|
||||
"Site Enabled: {active}".format(
|
||||
"Site Enabled: {active}\n"
|
||||
"mod_macro Vhost: {modmacro}".format(
|
||||
filename=self.filep,
|
||||
vhpath=self.path,
|
||||
addrs=", ".join(str(addr) for addr in self.addrs),
|
||||
name=self.name if self.name is not None else "",
|
||||
aliases=", ".join(name for name in self.aliases),
|
||||
tls="Yes" if self.ssl else "No",
|
||||
active="Yes" if self.enabled else "No"))
|
||||
active="Yes" if self.enabled else "No",
|
||||
modmacro="Yes" if self.modmacro else "No"))
|
||||
|
||||
def __eq__(self, other):
|
||||
if isinstance(other, self.__class__):
|
||||
return (self.filep == other.filep and self.path == other.path and
|
||||
self.addrs == other.addrs and
|
||||
self.get_names() == other.get_names() and
|
||||
self.ssl == other.ssl and self.enabled == other.enabled)
|
||||
self.ssl == other.ssl and
|
||||
self.enabled == other.enabled and
|
||||
self.modmacro == other.modmacro)
|
||||
|
||||
return False
|
||||
|
||||
|
||||
@@ -8,12 +8,6 @@ SSLCipherSuite ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA25
|
||||
SSLHonorCipherOrder on
|
||||
SSLCompression off
|
||||
|
||||
|
||||
ServerSignature Off
|
||||
AcceptPathInfo Off
|
||||
AddOutputFilterByType DEFLATE text/html text/plain text/xml application/pdf
|
||||
AddDefaultCharset UTF-8
|
||||
|
||||
SSLOptions +StrictRequire
|
||||
|
||||
# Add vhost name to log entries:
|
||||
|
||||
@@ -59,14 +59,20 @@ class TwoVhost80Test(util.ApacheTest):
|
||||
# Weak test..
|
||||
ApacheConfigurator.add_parser_arguments(mock.MagicMock())
|
||||
|
||||
def test_get_all_names(self):
|
||||
@mock.patch("zope.component.getUtility")
|
||||
def test_get_all_names(self, mock_getutility):
|
||||
mock_getutility.notification = mock.MagicMock(return_value=True)
|
||||
names = self.config.get_all_names()
|
||||
self.assertEqual(names, set(
|
||||
["letsencrypt.demo", "encryption-example.demo", "ip-172-30-0-17"]))
|
||||
|
||||
@mock.patch("zope.component.getUtility")
|
||||
@mock.patch("letsencrypt_apache.configurator.socket.gethostbyaddr")
|
||||
def test_get_all_names_addrs(self, mock_gethost):
|
||||
def test_get_all_names_addrs(self, mock_gethost, mock_getutility):
|
||||
mock_gethost.side_effect = [("google.com", "", ""), socket.error]
|
||||
notification = mock.Mock()
|
||||
notification.notification = mock.Mock(return_value=True)
|
||||
mock_getutility.return_value = notification
|
||||
vhost = obj.VirtualHost(
|
||||
"fp", "ap",
|
||||
set([obj.Addr(("8.8.8.8", "443")),
|
||||
@@ -97,7 +103,7 @@ class TwoVhost80Test(util.ApacheTest):
|
||||
|
||||
"""
|
||||
vhs = self.config.get_virtual_hosts()
|
||||
self.assertEqual(len(vhs), 4)
|
||||
self.assertEqual(len(vhs), 5)
|
||||
found = 0
|
||||
|
||||
for vhost in vhs:
|
||||
@@ -108,7 +114,7 @@ class TwoVhost80Test(util.ApacheTest):
|
||||
else:
|
||||
raise Exception("Missed: %s" % vhost) # pragma: no cover
|
||||
|
||||
self.assertEqual(found, 4)
|
||||
self.assertEqual(found, 5)
|
||||
|
||||
@mock.patch("letsencrypt_apache.display_ops.select_vhost")
|
||||
def test_choose_vhost_none_avail(self, mock_select):
|
||||
@@ -174,7 +180,7 @@ class TwoVhost80Test(util.ApacheTest):
|
||||
|
||||
def test_non_default_vhosts(self):
|
||||
# pylint: disable=protected-access
|
||||
self.assertEqual(len(self.config._non_default_vhosts()), 3)
|
||||
self.assertEqual(len(self.config._non_default_vhosts()), 4)
|
||||
|
||||
def test_is_site_enabled(self):
|
||||
"""Test if site is enabled.
|
||||
@@ -345,7 +351,7 @@ class TwoVhost80Test(util.ApacheTest):
|
||||
self.assertEqual(self.config.is_name_vhost(self.vh_truth[0]),
|
||||
self.config.is_name_vhost(ssl_vhost))
|
||||
|
||||
self.assertEqual(len(self.config.vhosts), 5)
|
||||
self.assertEqual(len(self.config.vhosts), 6)
|
||||
|
||||
def test_make_vhost_ssl_extra_vhs(self):
|
||||
self.config.aug.match = mock.Mock(return_value=["p1", "p2"])
|
||||
@@ -382,8 +388,8 @@ class TwoVhost80Test(util.ApacheTest):
|
||||
account_key, achall1, achall2 = self.get_achalls()
|
||||
|
||||
dvsni_ret_val = [
|
||||
achall1.gen_response(account_key),
|
||||
achall2.gen_response(account_key),
|
||||
achall1.response(account_key),
|
||||
achall2.response(account_key),
|
||||
]
|
||||
|
||||
mock_dvsni_perform.return_value = dvsni_ret_val
|
||||
@@ -492,10 +498,10 @@ class TwoVhost80Test(util.ApacheTest):
|
||||
def test_get_chall_pref(self):
|
||||
self.assertTrue(isinstance(self.config.get_chall_pref(""), list))
|
||||
|
||||
def test_temp_install(self):
|
||||
from letsencrypt_apache.configurator import temp_install
|
||||
def test_install_ssl_options_conf(self):
|
||||
from letsencrypt_apache.configurator import install_ssl_options_conf
|
||||
path = os.path.join(self.work_dir, "test_it")
|
||||
temp_install(path)
|
||||
install_ssl_options_conf(path)
|
||||
self.assertTrue(os.path.isfile(path))
|
||||
|
||||
# TEST ENHANCEMENTS
|
||||
@@ -665,20 +671,20 @@ class TwoVhost80Test(util.ApacheTest):
|
||||
self.vh_truth[1].aliases = set(["yes.default.com"])
|
||||
|
||||
self.config._enable_redirect(self.vh_truth[1], "") # pylint: disable=protected-access
|
||||
self.assertEqual(len(self.config.vhosts), 5)
|
||||
self.assertEqual(len(self.config.vhosts), 6)
|
||||
|
||||
def get_achalls(self):
|
||||
"""Return testing achallenges."""
|
||||
account_key = self.rsa512jwk
|
||||
achall1 = achallenges.DVSNI(
|
||||
achall1 = achallenges.KeyAuthorizationAnnotatedChallenge(
|
||||
challb=acme_util.chall_to_challb(
|
||||
challenges.DVSNI(
|
||||
challenges.TLSSNI01(
|
||||
token="jIq_Xy1mXGN37tb4L6Xj_es58fW571ZNyXekdZzhh7Q"),
|
||||
"pending"),
|
||||
domain="encryption-example.demo", account_key=account_key)
|
||||
achall2 = achallenges.DVSNI(
|
||||
achall2 = achallenges.KeyAuthorizationAnnotatedChallenge(
|
||||
challb=acme_util.chall_to_challb(
|
||||
challenges.DVSNI(
|
||||
challenges.TLSSNI01(
|
||||
token="uqnaPzxtrndteOqtrXb0Asl5gOJfWAnnx6QJyvcmlDU"),
|
||||
"pending"),
|
||||
domain="letsencrypt.demo", account_key=account_key)
|
||||
|
||||
@@ -57,7 +57,7 @@ class SelectVhostTest(unittest.TestCase):
|
||||
|
||||
@mock.patch("letsencrypt_apache.display_ops.zope.component.getUtility")
|
||||
def test_multiple_names(self, mock_util):
|
||||
mock_util().menu.return_value = (display_util.OK, 4)
|
||||
mock_util().menu.return_value = (display_util.OK, 5)
|
||||
|
||||
self.vhosts.append(
|
||||
obj.VirtualHost(
|
||||
@@ -65,7 +65,7 @@ class SelectVhostTest(unittest.TestCase):
|
||||
False, False,
|
||||
"wildcard.com", set(["*.wildcard.com"])))
|
||||
|
||||
self.assertEqual(self.vhosts[4], self._call(self.vhosts))
|
||||
self.assertEqual(self.vhosts[5], self._call(self.vhosts))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -13,15 +13,15 @@ from letsencrypt_apache.tests import util
|
||||
class DvsniPerformTest(util.ApacheTest):
|
||||
"""Test the ApacheDVSNI challenge."""
|
||||
|
||||
auth_key = common_test.DvsniTest.auth_key
|
||||
achalls = common_test.DvsniTest.achalls
|
||||
auth_key = common_test.TLSSNI01Test.auth_key
|
||||
achalls = common_test.TLSSNI01Test.achalls
|
||||
|
||||
def setUp(self): # pylint: disable=arguments-differ
|
||||
super(DvsniPerformTest, self).setUp()
|
||||
|
||||
config = util.get_apache_configurator(
|
||||
self.config_path, self.config_dir, self.work_dir)
|
||||
config.config.dvsni_port = 443
|
||||
config.config.tls_sni_01_port = 443
|
||||
|
||||
from letsencrypt_apache import dvsni
|
||||
self.sni = dvsni.ApacheDvsni(config)
|
||||
@@ -46,7 +46,7 @@ class DvsniPerformTest(util.ApacheTest):
|
||||
|
||||
achall = self.achalls[0]
|
||||
self.sni.add_chall(achall)
|
||||
response = self.achalls[0].gen_response(self.auth_key)
|
||||
response = self.achalls[0].response(self.auth_key)
|
||||
mock_setup_cert = mock.MagicMock(return_value=response)
|
||||
# pylint: disable=protected-access
|
||||
self.sni._setup_challenge_cert = mock_setup_cert
|
||||
@@ -72,7 +72,7 @@ class DvsniPerformTest(util.ApacheTest):
|
||||
acme_responses = []
|
||||
for achall in self.achalls:
|
||||
self.sni.add_chall(achall)
|
||||
acme_responses.append(achall.gen_response(self.auth_key))
|
||||
acme_responses.append(achall.response(self.auth_key))
|
||||
|
||||
mock_setup_cert = mock.MagicMock(side_effect=acme_responses)
|
||||
# pylint: disable=protected-access
|
||||
@@ -100,7 +100,7 @@ class DvsniPerformTest(util.ApacheTest):
|
||||
z_domains = []
|
||||
for achall in self.achalls:
|
||||
self.sni.add_chall(achall)
|
||||
z_domain = achall.gen_response(self.auth_key).z_domain
|
||||
z_domain = achall.response(self.auth_key).z_domain
|
||||
z_domains.append(set([z_domain]))
|
||||
|
||||
self.sni._mod_config() # pylint: disable=protected-access
|
||||
|
||||
@@ -52,7 +52,7 @@ class BasicParserTest(util.ParserTest):
|
||||
test2 = self.parser.find_dir("documentroot")
|
||||
|
||||
self.assertEqual(len(test), 1)
|
||||
self.assertEqual(len(test2), 3)
|
||||
self.assertEqual(len(test2), 4)
|
||||
|
||||
def test_add_dir(self):
|
||||
aug_default = "/files" + self.parser.loc["default"]
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
<Macro VHost $name $domain>
|
||||
<VirtualHost *:80>
|
||||
ServerName $domain
|
||||
ServerAlias www.$domain
|
||||
DocumentRoot /var/www/html
|
||||
|
||||
ErrorLog ${APACHE_LOG_DIR}/error.log
|
||||
CustomLog ${APACHE_LOG_DIR}/access.log combined
|
||||
</VirtualHost>
|
||||
</Macro>
|
||||
Use VHost macro1 test.com
|
||||
Use VHost macro2 hostname.org
|
||||
Use VHost macro3 apache.org
|
||||
|
||||
# vim: syntax=apache ts=4 sw=4 sts=4 sr noet
|
||||
@@ -0,0 +1 @@
|
||||
../sites-available/mod_macro-example.conf
|
||||
@@ -124,6 +124,11 @@ def get_vh_truth(temp_dir, config_name):
|
||||
os.path.join(aug_pre, "letsencrypt.conf/VirtualHost"),
|
||||
set([obj.Addr.fromstring("*:80")]), False, True,
|
||||
"letsencrypt.demo"),
|
||||
obj.VirtualHost(
|
||||
os.path.join(prefix, "mod_macro-example.conf"),
|
||||
os.path.join(aug_pre,
|
||||
"mod_macro-example.conf/Macro/VirtualHost"),
|
||||
set([obj.Addr.fromstring("*:80")]), False, True, modmacro=True)
|
||||
]
|
||||
return vh_truth
|
||||
|
||||
|
||||
@@ -8,17 +8,16 @@
|
||||
# without requiring specific versions of its dependencies from the operating
|
||||
# system.
|
||||
|
||||
# Note: you can set XDG_DATA_HOME or VENV_PATH before running this script,
|
||||
# if you want to change where the virtual environment will be installed
|
||||
XDG_DATA_HOME=${XDG_DATA_HOME:-~/.local/share}
|
||||
VENV_NAME="letsencrypt"
|
||||
VENV_PATH=${VENV_PATH:-"$XDG_DATA_HOME/$VENV_NAME"}
|
||||
VENV_BIN=${VENV_PATH}/bin
|
||||
|
||||
if test "`id -u`" -ne "0" ; then
|
||||
SUDO=sudo
|
||||
else
|
||||
SUDO=
|
||||
fi
|
||||
|
||||
# This script takes the same arguments as the main letsencrypt program, but it
|
||||
# additionally responds to --verbose (more output) and --debug (allow support
|
||||
# for experimental platforms)
|
||||
for arg in "$@" ; do
|
||||
# This first clause is redundant with the third, but hedging on portability
|
||||
if [ "$arg" = "-v" ] || [ "$arg" = "--verbose" ] || echo "$arg" | grep -E -- "-v+$" ; then
|
||||
@@ -28,12 +27,52 @@ for arg in "$@" ; do
|
||||
fi
|
||||
done
|
||||
|
||||
# letsencrypt-auto needs root access to bootstrap OS dependencies, and
|
||||
# letsencrypt itself needs root access for almost all modes of operation
|
||||
# The "normal" case is that sudo is used for the steps that need root, but
|
||||
# this script *can* be run as root (not recommended), or fall back to using
|
||||
# `su`
|
||||
if test "`id -u`" -ne "0" ; then
|
||||
if command -v sudo 1>/dev/null 2>&1; then
|
||||
SUDO=sudo
|
||||
else
|
||||
echo \"sudo\" is not available, will use \"su\" for installation steps...
|
||||
# Because the parameters in `su -c` has to be a string,
|
||||
# we need properly escape it
|
||||
su_sudo() {
|
||||
args=""
|
||||
# This `while` loop iterates over all parameters given to this function.
|
||||
# For each parameter, all `'` will be replace by `'"'"'`, and the escaped string
|
||||
# will be wrap in a pair of `'`, then append to `$args` string
|
||||
# For example, `echo "It's only 1\$\!"` will be escaped to:
|
||||
# 'echo' 'It'"'"'s only 1$!'
|
||||
# │ │└┼┘│
|
||||
# │ │ │ └── `'s only 1$!'` the literal string
|
||||
# │ │ └── `\"'\"` is a single quote (as a string)
|
||||
# │ └── `'It'`, to be concatenated with the strings followed it
|
||||
# └── `echo` wrapped in a pair of `'`, it's totally fine for the shell command itself
|
||||
while [ $# -ne 0 ]; do
|
||||
args="$args'$(printf "%s" "$1" | sed -e "s/'/'\"'\"'/g")' "
|
||||
shift
|
||||
done
|
||||
su root -c "$args"
|
||||
}
|
||||
SUDO=su_sudo
|
||||
fi
|
||||
else
|
||||
SUDO=
|
||||
fi
|
||||
|
||||
ExperimentalBootstrap() {
|
||||
# Arguments: Platform name, boostrap script name, SUDO command (iff needed)
|
||||
if [ "$DEBUG" = 1 ] ; then
|
||||
if [ "$2" != "" ] ; then
|
||||
echo "Bootstrapping dependencies for $1..."
|
||||
"$3" "$BOOTSTRAP/$2"
|
||||
if [ "$3" != "" ] ; then
|
||||
"$3" "$BOOTSTRAP/$2"
|
||||
else
|
||||
"$BOOTSTRAP/$2"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
echo "WARNING: $1 support is very experimental at present..."
|
||||
@@ -43,13 +82,12 @@ ExperimentalBootstrap() {
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
DeterminePythonVersion() {
|
||||
if which python2 > /dev/null ; then
|
||||
export LE_PYTHON=${LE_PYTHON:-python2}
|
||||
elif which python2.7 > /dev/null ; then
|
||||
if command -v python2.7 > /dev/null ; then
|
||||
export LE_PYTHON=${LE_PYTHON:-python2.7}
|
||||
elif which python > /dev/null ; then
|
||||
elif command -v python2 > /dev/null ; then
|
||||
export LE_PYTHON=${LE_PYTHON:-python2}
|
||||
elif command -v python > /dev/null ; then
|
||||
export LE_PYTHON=${LE_PYTHON:-python}
|
||||
else
|
||||
echo "Cannot find any Pythons... please install one!"
|
||||
@@ -82,6 +120,9 @@ then
|
||||
elif [ -f /etc/redhat-release ] ; then
|
||||
echo "Bootstrapping dependencies for RedHat-based OSes..."
|
||||
$SUDO $BOOTSTRAP/_rpm_common.sh
|
||||
elif `grep -q openSUSE /etc/os-release` ; then
|
||||
echo "Bootstrapping dependencies for openSUSE-based OSes..."
|
||||
$SUDO $BOOTSTRAP/_suse_common.sh
|
||||
elif [ -f /etc/arch-release ] ; then
|
||||
echo "Bootstrapping dependencies for Archlinux..."
|
||||
$SUDO $BOOTSTRAP/archlinux.sh
|
||||
@@ -93,6 +134,8 @@ then
|
||||
ExperimentalBootstrap "FreeBSD" freebsd.sh "$SUDO"
|
||||
elif uname | grep -iq Darwin ; then
|
||||
ExperimentalBootstrap "Mac OS X" mac.sh
|
||||
elif grep -iq "Amazon Linux" /etc/issue ; then
|
||||
ExperimentalBootstrap "Amazon Linux" amazon_linux.sh
|
||||
else
|
||||
echo "Sorry, I don't know how to bootstrap Let's Encrypt on your operating system!"
|
||||
echo
|
||||
@@ -130,7 +173,7 @@ else
|
||||
$VENV_BIN/pip install -U pip > /dev/null
|
||||
printf .
|
||||
# nginx is buggy / disabled for now...
|
||||
$VENV_BIN/pip install -r py26reqs.txt
|
||||
$VENV_BIN/pip install -r py26reqs.txt > /dev/null
|
||||
printf .
|
||||
$VENV_BIN/pip install -U letsencrypt > /dev/null
|
||||
printf .
|
||||
|
||||
@@ -226,25 +226,26 @@ htmlhelp_basename = 'letsencrypt-compatibility-testdoc'
|
||||
# -- Options for LaTeX output ---------------------------------------------
|
||||
|
||||
latex_elements = {
|
||||
# The paper size ('letterpaper' or 'a4paper').
|
||||
#'papersize': 'letterpaper',
|
||||
# The paper size ('letterpaper' or 'a4paper').
|
||||
#'papersize': 'letterpaper',
|
||||
|
||||
# The font size ('10pt', '11pt' or '12pt').
|
||||
#'pointsize': '10pt',
|
||||
# The font size ('10pt', '11pt' or '12pt').
|
||||
#'pointsize': '10pt',
|
||||
|
||||
# Additional stuff for the LaTeX preamble.
|
||||
#'preamble': '',
|
||||
# Additional stuff for the LaTeX preamble.
|
||||
#'preamble': '',
|
||||
|
||||
# Latex figure (float) alignment
|
||||
#'figure_align': 'htbp',
|
||||
# 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, 'letsencrypt-compatibility-test.tex', u'letsencrypt-compatibility-test Documentation',
|
||||
u'Let\'s Encrypt Project', 'manual'),
|
||||
(master_doc, 'letsencrypt-compatibility-test.tex',
|
||||
u'letsencrypt-compatibility-test Documentation',
|
||||
u'Let\'s Encrypt Project', 'manual'),
|
||||
]
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top of
|
||||
@@ -273,7 +274,8 @@ latex_documents = [
|
||||
# One entry per manual page. List of tuples
|
||||
# (source start file, name, description, authors, manual section).
|
||||
man_pages = [
|
||||
(master_doc, 'letsencrypt-compatibility-test', u'letsencrypt-compatibility-test Documentation',
|
||||
(master_doc, 'letsencrypt-compatibility-test',
|
||||
u'letsencrypt-compatibility-test Documentation',
|
||||
[author], 1)
|
||||
]
|
||||
|
||||
@@ -287,9 +289,10 @@ man_pages = [
|
||||
# (source start file, target name, title, author,
|
||||
# dir menu entry, description, category)
|
||||
texinfo_documents = [
|
||||
(master_doc, 'letsencrypt-compatibility-test', u'letsencrypt-compatibility-test Documentation',
|
||||
author, 'letsencrypt-compatibility-test', 'One line description of project.',
|
||||
'Miscellaneous'),
|
||||
(master_doc, 'letsencrypt-compatibility-test',
|
||||
u'letsencrypt-compatibility-test Documentation',
|
||||
author, 'letsencrypt-compatibility-test',
|
||||
'One line description of project.', 'Miscellaneous'),
|
||||
]
|
||||
|
||||
# Documents to append as an appendix to all manuals.
|
||||
@@ -309,6 +312,8 @@ intersphinx_mapping = {
|
||||
'python': ('https://docs.python.org/', None),
|
||||
'acme': ('https://acme-python.readthedocs.org/en/latest/', None),
|
||||
'letsencrypt': ('https://letsencrypt.readthedocs.org/en/latest/', None),
|
||||
'letsencrypt-apache': ('https://letsencrypt-apache.readthedocs.org/en/latest/', None),
|
||||
'letsencrypt-nginx': ('https://letsencrypt-nginx.readthedocs.org/en/latest/', None),
|
||||
'letsencrypt-apache': (
|
||||
'https://letsencrypt-apache.readthedocs.org/en/latest/', None),
|
||||
'letsencrypt-nginx': (
|
||||
'https://letsencrypt-nginx.readthedocs.org/en/latest/', None),
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ def test_authenticator(plugin, config, temp_dir):
|
||||
"Plugin failed to complete %s for %s in %s",
|
||||
type(achalls[i]), achalls[i].domain, config)
|
||||
success = False
|
||||
elif isinstance(responses[i], challenges.DVSNIResponse):
|
||||
elif isinstance(responses[i], challenges.TLSSNI01):
|
||||
verify = functools.partial(responses[i].simple_verify, achalls[i],
|
||||
achalls[i].domain,
|
||||
util.JWK.public_key(),
|
||||
@@ -68,10 +68,10 @@ def test_authenticator(plugin, config, temp_dir):
|
||||
port=plugin.https_port)
|
||||
if _try_until_true(verify):
|
||||
logger.info(
|
||||
"DVSNI verification for %s succeeded", achalls[i].domain)
|
||||
"tls-sni-01 verification for %s succeeded", achalls[i].domain)
|
||||
else:
|
||||
logger.error(
|
||||
"DVSNI verification for %s in %s failed",
|
||||
"tls-sni-01 verification for %s in %s failed",
|
||||
achalls[i].domain, config)
|
||||
success = False
|
||||
|
||||
@@ -99,12 +99,12 @@ def _create_achalls(plugin):
|
||||
for domain in names:
|
||||
prefs = plugin.get_chall_pref(domain)
|
||||
for chall_type in prefs:
|
||||
if chall_type == challenges.DVSNI:
|
||||
chall = challenges.DVSNI(
|
||||
token=os.urandom(challenges.DVSNI.TOKEN_SIZE))
|
||||
if chall_type == challenges.TLSSNI01:
|
||||
chall = challenges.TLSSNI01(
|
||||
token=os.urandom(challenges.TLSSNI01.TOKEN_SIZE))
|
||||
challb = acme_util.chall_to_challb(
|
||||
chall, messages.STATUS_PENDING)
|
||||
achall = achallenges.DVSNI(
|
||||
achall = achallenges.KeyAuthorizationAnnotatedChallenge(
|
||||
challb=challb, domain=domain, account_key=util.JWK)
|
||||
achalls.append(achall)
|
||||
|
||||
|
||||
@@ -225,25 +225,25 @@ htmlhelp_basename = 'letsencrypt-nginxdoc'
|
||||
# -- Options for LaTeX output ---------------------------------------------
|
||||
|
||||
latex_elements = {
|
||||
# The paper size ('letterpaper' or 'a4paper').
|
||||
#'papersize': 'letterpaper',
|
||||
# The paper size ('letterpaper' or 'a4paper').
|
||||
#'papersize': 'letterpaper',
|
||||
|
||||
# The font size ('10pt', '11pt' or '12pt').
|
||||
#'pointsize': '10pt',
|
||||
# The font size ('10pt', '11pt' or '12pt').
|
||||
#'pointsize': '10pt',
|
||||
|
||||
# Additional stuff for the LaTeX preamble.
|
||||
#'preamble': '',
|
||||
# Additional stuff for the LaTeX preamble.
|
||||
#'preamble': '',
|
||||
|
||||
# Latex figure (float) alignment
|
||||
#'figure_align': 'htbp',
|
||||
# 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, 'letsencrypt-nginx.tex', u'letsencrypt-nginx Documentation',
|
||||
u'Let\'s Encrypt Project', 'manual'),
|
||||
(master_doc, 'letsencrypt-nginx.tex', u'letsencrypt-nginx Documentation',
|
||||
u'Let\'s Encrypt Project', 'manual'),
|
||||
]
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top of
|
||||
@@ -286,9 +286,9 @@ man_pages = [
|
||||
# (source start file, target name, title, author,
|
||||
# dir menu entry, description, category)
|
||||
texinfo_documents = [
|
||||
(master_doc, 'letsencrypt-nginx', u'letsencrypt-nginx Documentation',
|
||||
author, 'letsencrypt-nginx', 'One line description of project.',
|
||||
'Miscellaneous'),
|
||||
(master_doc, 'letsencrypt-nginx', u'letsencrypt-nginx Documentation',
|
||||
author, 'letsencrypt-nginx', 'One line description of project.',
|
||||
'Miscellaneous'),
|
||||
]
|
||||
|
||||
# Documents to append as an appendix to all manuals.
|
||||
|
||||
@@ -14,7 +14,6 @@ import zope.interface
|
||||
from acme import challenges
|
||||
from acme import crypto_util as acme_crypto_util
|
||||
|
||||
from letsencrypt import achallenges
|
||||
from letsencrypt import constants as core_constants
|
||||
from letsencrypt import crypto_util
|
||||
from letsencrypt import errors
|
||||
@@ -108,6 +107,10 @@ class NginxConfigurator(common.Plugin):
|
||||
# This is called in determine_authenticator and determine_installer
|
||||
def prepare(self):
|
||||
"""Prepare the authenticator/installer."""
|
||||
# Verify Nginx is installed
|
||||
if not le_util.exe_exists(self.conf('ctl')):
|
||||
raise errors.NoInstallationError
|
||||
|
||||
self.parser = parser.NginxParser(
|
||||
self.conf('server-root'), self.mod_ssl_conf)
|
||||
|
||||
@@ -297,7 +300,7 @@ class NginxConfigurator(common.Plugin):
|
||||
"""Make a server SSL.
|
||||
|
||||
Make a server SSL based on server_name and filename by adding a
|
||||
``listen IConfig.dvsni_port ssl`` directive to the server block.
|
||||
``listen IConfig.tls_sni_01_port ssl`` directive to the server block.
|
||||
|
||||
.. todo:: Maybe this should create a new block instead of modifying
|
||||
the existing one?
|
||||
@@ -307,7 +310,7 @@ class NginxConfigurator(common.Plugin):
|
||||
|
||||
"""
|
||||
snakeoil_cert, snakeoil_key = self._get_snakeoil_paths()
|
||||
ssl_block = [['listen', '{0} ssl'.format(self.config.dvsni_port)],
|
||||
ssl_block = [['listen', '{0} ssl'.format(self.config.tls_sni_01_port)],
|
||||
# access and error logs necessary for integration
|
||||
# testing (non-root)
|
||||
['access_log', os.path.join(
|
||||
@@ -321,7 +324,8 @@ class NginxConfigurator(common.Plugin):
|
||||
vhost.filep, vhost.names, ssl_block)
|
||||
vhost.ssl = True
|
||||
vhost.raw.extend(ssl_block)
|
||||
vhost.addrs.add(obj.Addr('', str(self.config.dvsni_port), True, False))
|
||||
vhost.addrs.add(obj.Addr(
|
||||
'', str(self.config.tls_sni_01_port), True, False))
|
||||
|
||||
def get_all_certs_keys(self):
|
||||
"""Find all existing keys, certs from configuration.
|
||||
@@ -536,7 +540,7 @@ class NginxConfigurator(common.Plugin):
|
||||
###########################################################################
|
||||
def get_chall_pref(self, unused_domain): # pylint: disable=no-self-use
|
||||
"""Return list of challenge preferences."""
|
||||
return [challenges.DVSNI]
|
||||
return [challenges.TLSSNI01]
|
||||
|
||||
# Entry point in main.py for performing challenges
|
||||
def perform(self, achalls):
|
||||
@@ -552,11 +556,10 @@ class NginxConfigurator(common.Plugin):
|
||||
nginx_dvsni = dvsni.NginxDvsni(self)
|
||||
|
||||
for i, achall in enumerate(achalls):
|
||||
if isinstance(achall, achallenges.DVSNI):
|
||||
# Currently also have dvsni hold associated index
|
||||
# of the challenge. This helps to put all of the responses back
|
||||
# together when they are all complete.
|
||||
nginx_dvsni.add_chall(achall, i)
|
||||
# Currently also have dvsni hold associated index
|
||||
# of the challenge. This helps to put all of the responses back
|
||||
# together when they are all complete.
|
||||
nginx_dvsni.add_chall(achall, i)
|
||||
|
||||
sni_response = nginx_dvsni.perform()
|
||||
# Must restart in order to activate the challenges.
|
||||
|
||||
@@ -13,7 +13,7 @@ from letsencrypt_nginx import nginxparser
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class NginxDvsni(common.Dvsni):
|
||||
class NginxDvsni(common.TLSSNI01):
|
||||
"""Class performs DVSNI challenges within the Nginx configurator.
|
||||
|
||||
:ivar configurator: NginxConfigurator object
|
||||
@@ -48,7 +48,7 @@ class NginxDvsni(common.Dvsni):
|
||||
|
||||
addresses = []
|
||||
default_addr = "{0} default_server ssl".format(
|
||||
self.configurator.config.dvsni_port)
|
||||
self.configurator.config.tls_sni_01_port)
|
||||
|
||||
for achall in self.achalls:
|
||||
vhost = self.configurator.choose_vhost(achall.domain)
|
||||
@@ -99,8 +99,8 @@ class NginxDvsni(common.Dvsni):
|
||||
for key, body in main:
|
||||
if key == ['http']:
|
||||
found_bucket = False
|
||||
for key, _ in body:
|
||||
if key == bucket_directive[0]:
|
||||
for k, _ in body:
|
||||
if k == bucket_directive[0]:
|
||||
found_bucket = True
|
||||
if not found_bucket:
|
||||
body.insert(0, bucket_directive)
|
||||
@@ -141,7 +141,7 @@ class NginxDvsni(common.Dvsni):
|
||||
block = [['listen', str(addr)] for addr in addrs]
|
||||
|
||||
block.extend([['server_name',
|
||||
achall.gen_response(achall.account_key).z_domain],
|
||||
achall.response(achall.account_key).z_domain],
|
||||
['include', self.configurator.parser.loc["ssl_options"]],
|
||||
# access and error logs necessary for
|
||||
# integration testing (non-root)
|
||||
|
||||
@@ -246,7 +246,7 @@ class NginxParser(object):
|
||||
# Can't be a server block
|
||||
return False
|
||||
|
||||
if item[0] == 'server_name':
|
||||
if len(item) > 0 and item[0] == 'server_name':
|
||||
server_names.update(_get_servernames(item[1]))
|
||||
|
||||
return server_names == names
|
||||
@@ -425,7 +425,7 @@ def _is_include_directive(entry):
|
||||
|
||||
"""
|
||||
return (isinstance(entry, list) and
|
||||
entry[0] == 'include' and len(entry) == 2 and
|
||||
len(entry) == 2 and entry[0] == 'include' and
|
||||
isinstance(entry[1], str))
|
||||
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
# pylint: disable=too-many-public-methods
|
||||
"""Test for letsencrypt_nginx.configurator."""
|
||||
import os
|
||||
import shutil
|
||||
@@ -29,6 +30,12 @@ class NginxConfiguratorTest(util.NginxTest):
|
||||
shutil.rmtree(self.config_dir)
|
||||
shutil.rmtree(self.work_dir)
|
||||
|
||||
@mock.patch("letsencrypt_nginx.configurator.le_util.exe_exists")
|
||||
def test_prepare_no_install(self, mock_exe_exists):
|
||||
mock_exe_exists.return_value = False
|
||||
self.assertRaises(
|
||||
errors.NoInstallationError, self.config.prepare)
|
||||
|
||||
def test_prepare(self):
|
||||
self.assertEquals((1, 6, 2), self.config.version)
|
||||
self.assertEquals(5, len(self.config.parser.parsed))
|
||||
@@ -51,7 +58,7 @@ class NginxConfiguratorTest(util.NginxTest):
|
||||
errors.PluginError, self.config.enhance, 'myhost', 'redirect')
|
||||
|
||||
def test_get_chall_pref(self):
|
||||
self.assertEqual([challenges.DVSNI],
|
||||
self.assertEqual([challenges.TLSSNI01],
|
||||
self.config.get_chall_pref('myhost'))
|
||||
|
||||
def test_save(self):
|
||||
@@ -210,22 +217,22 @@ class NginxConfiguratorTest(util.NginxTest):
|
||||
def test_perform(self, mock_restart, mock_dvsni_perform):
|
||||
# Only tests functionality specific to configurator.perform
|
||||
# Note: As more challenges are offered this will have to be expanded
|
||||
achall1 = achallenges.DVSNI(
|
||||
achall1 = achallenges.KeyAuthorizationAnnotatedChallenge(
|
||||
challb=messages.ChallengeBody(
|
||||
chall=challenges.DVSNI(token="kNdwjwOeX0I_A8DXt9Msmg"),
|
||||
chall=challenges.TLSSNI01(token="kNdwjwOeX0I_A8DXt9Msmg"),
|
||||
uri="https://ca.org/chall0_uri",
|
||||
status=messages.Status("pending"),
|
||||
), domain="localhost", account_key=self.rsa512jwk)
|
||||
achall2 = achallenges.DVSNI(
|
||||
achall2 = achallenges.KeyAuthorizationAnnotatedChallenge(
|
||||
challb=messages.ChallengeBody(
|
||||
chall=challenges.DVSNI(token="m8TdO1qik4JVFtgPPurJmg"),
|
||||
chall=challenges.TLSSNI01(token="m8TdO1qik4JVFtgPPurJmg"),
|
||||
uri="https://ca.org/chall1_uri",
|
||||
status=messages.Status("pending"),
|
||||
), domain="example.com", account_key=self.rsa512jwk)
|
||||
|
||||
dvsni_ret_val = [
|
||||
achall1.gen_response(self.rsa512jwk),
|
||||
achall2.gen_response(self.rsa512jwk),
|
||||
achall1.response(self.rsa512jwk),
|
||||
achall2.response(self.rsa512jwk),
|
||||
]
|
||||
|
||||
mock_dvsni_perform.return_value = dvsni_ret_val
|
||||
|
||||
@@ -19,22 +19,22 @@ from letsencrypt_nginx.tests import util
|
||||
class DvsniPerformTest(util.NginxTest):
|
||||
"""Test the NginxDVSNI challenge."""
|
||||
|
||||
account_key = common_test.DvsniTest.auth_key
|
||||
account_key = common_test.TLSSNI01Test.auth_key
|
||||
achalls = [
|
||||
achallenges.DVSNI(
|
||||
achallenges.KeyAuthorizationAnnotatedChallenge(
|
||||
challb=acme_util.chall_to_challb(
|
||||
challenges.DVSNI(token="kNdwjwOeX0I_A8DXt9Msmg"), "pending"),
|
||||
challenges.TLSSNI01(token="kNdwjwOeX0I_A8DXt9Msmg"), "pending"),
|
||||
domain="www.example.com", account_key=account_key),
|
||||
achallenges.DVSNI(
|
||||
achallenges.KeyAuthorizationAnnotatedChallenge(
|
||||
challb=acme_util.chall_to_challb(
|
||||
challenges.DVSNI(
|
||||
challenges.TLSSNI01(
|
||||
token="\xba\xa9\xda?<m\xaewmx\xea\xad\xadv\xf4\x02\xc9y"
|
||||
"\x80\xe2_X\t\xe7\xc7\xa4\t\xca\xf7&\x945"
|
||||
), "pending"),
|
||||
domain="blah", account_key=account_key),
|
||||
achallenges.DVSNI(
|
||||
achallenges.KeyAuthorizationAnnotatedChallenge(
|
||||
challb=acme_util.chall_to_challb(
|
||||
challenges.DVSNI(
|
||||
challenges.TLSSNI01(
|
||||
token="\x8c\x8a\xbf_-f\\cw\xee\xd6\xf8/\xa5\xe3\xfd"
|
||||
"\xeb9\xf1\xf5\xb9\xefVM\xc9w\xa4u\x9c\xe1\x87\xb4"
|
||||
), "pending"),
|
||||
@@ -70,7 +70,7 @@ class DvsniPerformTest(util.NginxTest):
|
||||
@mock.patch("letsencrypt_nginx.configurator.NginxConfigurator.save")
|
||||
def test_perform1(self, mock_save):
|
||||
self.sni.add_chall(self.achalls[0])
|
||||
response = self.achalls[0].gen_response(self.account_key)
|
||||
response = self.achalls[0].response(self.account_key)
|
||||
mock_setup_cert = mock.MagicMock(return_value=response)
|
||||
|
||||
# pylint: disable=protected-access
|
||||
@@ -92,7 +92,7 @@ class DvsniPerformTest(util.NginxTest):
|
||||
acme_responses = []
|
||||
for achall in self.achalls:
|
||||
self.sni.add_chall(achall)
|
||||
acme_responses.append(achall.gen_response(self.account_key))
|
||||
acme_responses.append(achall.response(self.account_key))
|
||||
|
||||
mock_setup_cert = mock.MagicMock(side_effect=acme_responses)
|
||||
# pylint: disable=protected-access
|
||||
@@ -139,9 +139,9 @@ class DvsniPerformTest(util.NginxTest):
|
||||
|
||||
for vhost in vhs:
|
||||
if vhost.addrs == set(v_addr1):
|
||||
response = self.achalls[0].gen_response(self.account_key)
|
||||
response = self.achalls[0].response(self.account_key)
|
||||
else:
|
||||
response = self.achalls[2].gen_response(self.account_key)
|
||||
response = self.achalls[2].response(self.account_key)
|
||||
self.assertEqual(vhost.addrs, set(v_addr2))
|
||||
self.assertEqual(vhost.names, set([response.z_domain]))
|
||||
|
||||
|
||||
@@ -49,21 +49,25 @@ def get_nginx_configurator(
|
||||
|
||||
backups = os.path.join(work_dir, "backups")
|
||||
|
||||
config = configurator.NginxConfigurator(
|
||||
config=mock.MagicMock(
|
||||
nginx_server_root=config_path,
|
||||
le_vhost_ext="-le-ssl.conf",
|
||||
config_dir=config_dir,
|
||||
work_dir=work_dir,
|
||||
backup_dir=backups,
|
||||
temp_checkpoint_dir=os.path.join(work_dir, "temp_checkpoints"),
|
||||
in_progress_dir=os.path.join(backups, "IN_PROGRESS"),
|
||||
server="https://acme-server.org:443/new",
|
||||
dvsni_port=5001,
|
||||
),
|
||||
name="nginx",
|
||||
version=version)
|
||||
config.prepare()
|
||||
with mock.patch("letsencrypt_nginx.configurator.le_util."
|
||||
"exe_exists") as mock_exe_exists:
|
||||
mock_exe_exists.return_value = True
|
||||
|
||||
config = configurator.NginxConfigurator(
|
||||
config=mock.MagicMock(
|
||||
nginx_server_root=config_path,
|
||||
le_vhost_ext="-le-ssl.conf",
|
||||
config_dir=config_dir,
|
||||
work_dir=work_dir,
|
||||
backup_dir=backups,
|
||||
temp_checkpoint_dir=os.path.join(work_dir, "temp_checkpoints"),
|
||||
in_progress_dir=os.path.join(backups, "IN_PROGRESS"),
|
||||
server="https://acme-server.org:443/new",
|
||||
tls_sni_01_port=5001,
|
||||
),
|
||||
name="nginx",
|
||||
version=version)
|
||||
config.prepare()
|
||||
|
||||
# Provide general config utility.
|
||||
nsconfig = configuration.NamespaceConfig(config.config)
|
||||
|
||||
@@ -49,40 +49,10 @@ class KeyAuthorizationAnnotatedChallenge(AnnotatedChallenge):
|
||||
"""Client annotated `KeyAuthorizationChallenge` challenge."""
|
||||
__slots__ = ('challb', 'domain', 'account_key')
|
||||
|
||||
def response_and_validation(self):
|
||||
def response_and_validation(self, *args, **kwargs):
|
||||
"""Generate response and validation."""
|
||||
return self.challb.chall.response_and_validation(self.account_key)
|
||||
|
||||
|
||||
class DVSNI(AnnotatedChallenge):
|
||||
"""Client annotated "dvsni" ACME challenge.
|
||||
|
||||
:ivar .JWK account_key: Authorized Account Key
|
||||
|
||||
"""
|
||||
__slots__ = ('challb', 'domain', 'account_key')
|
||||
acme_type = challenges.DVSNI
|
||||
|
||||
def gen_cert_and_response(self, key=None, bits=2048, alg=jose.RS256):
|
||||
"""Generate a DVSNI cert and response.
|
||||
|
||||
:param OpenSSL.crypto.PKey key: Private key used for
|
||||
certificate generation. If none provided, a fresh key will
|
||||
be generated.
|
||||
:param int bits: Number of bits for fresh key generation.
|
||||
:param .JWAAlgorithm alg:
|
||||
|
||||
:returns: ``(response, cert_pem, key_pem)`` tuple, where
|
||||
``response`` is an instance of
|
||||
`acme.challenges.DVSNIResponse`, ``cert`` is a certificate
|
||||
(`OpenSSL.crypto.X509`) and ``key`` is a private key
|
||||
(`OpenSSL.crypto.PKey`).
|
||||
:rtype: tuple
|
||||
|
||||
"""
|
||||
response = self.challb.chall.gen_response(self.account_key, alg=alg)
|
||||
cert, key = response.gen_cert(key=key, bits=bits)
|
||||
return response, cert, key
|
||||
return self.challb.chall.response_and_validation(
|
||||
self.account_key, *args, **kwargs)
|
||||
|
||||
|
||||
class DNS(AnnotatedChallenge):
|
||||
|
||||
@@ -344,8 +344,8 @@ def challb_to_achall(challb, account_key, domain):
|
||||
chall = challb.chall
|
||||
logger.info("%s challenge for %s", chall.typ, domain)
|
||||
|
||||
if isinstance(chall, challenges.DVSNI):
|
||||
return achallenges.DVSNI(
|
||||
if isinstance(chall, challenges.KeyAuthorizationChallenge):
|
||||
return achallenges.KeyAuthorizationAnnotatedChallenge(
|
||||
challb=challb, domain=domain, account_key=account_key)
|
||||
elif isinstance(chall, challenges.DNS):
|
||||
return achallenges.DNS(challb=challb, domain=domain)
|
||||
@@ -355,9 +355,6 @@ def challb_to_achall(challb, account_key, domain):
|
||||
elif isinstance(chall, challenges.ProofOfPossession):
|
||||
return achallenges.ProofOfPossession(
|
||||
challb=challb, domain=domain)
|
||||
elif isinstance(chall, challenges.KeyAuthorizationChallenge):
|
||||
return achallenges.KeyAuthorizationAnnotatedChallenge(
|
||||
challb=challb, domain=domain, account_key=account_key)
|
||||
else:
|
||||
raise errors.Error(
|
||||
"Received unsupported challenge of type: %s", chall.typ)
|
||||
|
||||
@@ -17,7 +17,6 @@ import zope.component
|
||||
import zope.interface.exceptions
|
||||
import zope.interface.verify
|
||||
|
||||
from acme import client as acme_client
|
||||
from acme import jose
|
||||
|
||||
import letsencrypt
|
||||
@@ -28,6 +27,7 @@ from letsencrypt import configuration
|
||||
from letsencrypt import constants
|
||||
from letsencrypt import client
|
||||
from letsencrypt import crypto_util
|
||||
from letsencrypt import errors
|
||||
from letsencrypt import interfaces
|
||||
from letsencrypt import le_util
|
||||
from letsencrypt import log
|
||||
@@ -36,10 +36,8 @@ from letsencrypt import storage
|
||||
|
||||
from letsencrypt.display import util as display_util
|
||||
from letsencrypt.display import ops as display_ops
|
||||
from letsencrypt.errors import Error, PluginSelectionError, CertStorageError
|
||||
from letsencrypt.plugins import disco as plugins_disco
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -106,8 +104,8 @@ def _find_domains(args, installer):
|
||||
domains = args.domains
|
||||
|
||||
if not domains:
|
||||
raise Error("Please specify --domains, or --installer that "
|
||||
"will help in domain names autodiscovery")
|
||||
raise errors.Error("Please specify --domains, or --installer that "
|
||||
"will help in domain names autodiscovery")
|
||||
|
||||
return domains
|
||||
|
||||
@@ -159,9 +157,9 @@ def _determine_account(args, config):
|
||||
try:
|
||||
acc, acme = client.register(
|
||||
config, account_storage, tos_cb=_tos_cb)
|
||||
except Error as error:
|
||||
except errors.Error as error:
|
||||
logger.debug(error, exc_info=True)
|
||||
raise Error(
|
||||
raise errors.Error(
|
||||
"Unable to register an account with ACME server")
|
||||
|
||||
args.account = acc.id
|
||||
@@ -195,7 +193,7 @@ def _find_duplicative_certs(config, domains):
|
||||
try:
|
||||
full_path = os.path.join(configs_dir, renewal_file)
|
||||
candidate_lineage = storage.RenewableCert(full_path, cli_config)
|
||||
except (CertStorageError, IOError):
|
||||
except (errors.CertStorageError, IOError):
|
||||
logger.warning("Renewal configuration file %s is broken. "
|
||||
"Skipping.", full_path)
|
||||
continue
|
||||
@@ -267,7 +265,7 @@ def _treat_as_renewal(config, domains):
|
||||
br=os.linesep
|
||||
),
|
||||
reporter_util.HIGH_PRIORITY)
|
||||
raise Error(
|
||||
raise errors.Error(
|
||||
"User did not use proper CLI and would like "
|
||||
"to reinvoke the client.")
|
||||
|
||||
@@ -304,7 +302,7 @@ def _report_new_cert(cert_path, fullchain_path):
|
||||
reporter_util.add_message(msg, reporter_util.MEDIUM_PRIORITY)
|
||||
|
||||
|
||||
def _auth_from_domains(le_client, config, domains, plugins):
|
||||
def _auth_from_domains(le_client, config, domains):
|
||||
"""Authenticate and enroll certificate."""
|
||||
# Note: This can raise errors... caught above us though.
|
||||
lineage = _treat_as_renewal(config, domains)
|
||||
@@ -325,9 +323,9 @@ def _auth_from_domains(le_client, config, domains, plugins):
|
||||
# configuration values from this attempt? <- Absolutely (jdkasten)
|
||||
else:
|
||||
# TREAT AS NEW REQUEST
|
||||
lineage = le_client.obtain_and_enroll_certificate(domains, plugins)
|
||||
lineage = le_client.obtain_and_enroll_certificate(domains)
|
||||
if not lineage:
|
||||
raise Error("Certificate could not be obtained")
|
||||
raise errors.Error("Certificate could not be obtained")
|
||||
|
||||
_report_new_cert(lineage.cert, lineage.fullchain)
|
||||
|
||||
@@ -346,7 +344,7 @@ def set_configurator(previously, now):
|
||||
if previously:
|
||||
if previously != now:
|
||||
msg = "Too many flags setting configurators/installers/authenticators {0} -> {1}"
|
||||
raise PluginSelectionError(msg.format(repr(previously), repr(now)))
|
||||
raise errors.PluginSelectionError(msg.format(repr(previously), repr(now)))
|
||||
return now
|
||||
|
||||
|
||||
@@ -379,7 +377,7 @@ def diagnose_configurator_problem(cfg_type, requested, plugins):
|
||||
'"letsencrypt-auto certonly" to get a cert you can install manually')
|
||||
else:
|
||||
msg = "{0} could not be determined or is not installed".format(cfg_type)
|
||||
raise PluginSelectionError(msg)
|
||||
raise errors.PluginSelectionError(msg)
|
||||
|
||||
|
||||
def choose_configurator_plugins(args, config, plugins, verb):
|
||||
@@ -425,21 +423,30 @@ def choose_configurator_plugins(args, config, plugins, verb):
|
||||
authenticator = display_ops.pick_authenticator(config, req_auth, plugins)
|
||||
logger.debug("Selected authenticator %s and installer %s", authenticator, installer)
|
||||
|
||||
# Report on any failures
|
||||
if need_inst and not installer:
|
||||
diagnose_configurator_problem("installer", req_inst, plugins)
|
||||
if need_auth and not authenticator:
|
||||
diagnose_configurator_problem("authenticator", req_auth, plugins)
|
||||
|
||||
record_chosen_plugins(config, plugins, authenticator, installer)
|
||||
return installer, authenticator
|
||||
|
||||
|
||||
def record_chosen_plugins(config, plugins, auth, inst):
|
||||
"Update the config entries to reflect the plugins we actually selected."
|
||||
cn = config.namespace
|
||||
cn.authenticator = plugins.find_init(auth).name if auth else "none"
|
||||
cn.installer = plugins.find_init(inst).name if inst else "none"
|
||||
|
||||
|
||||
# TODO: Make run as close to auth + install as possible
|
||||
# Possible difficulties: args.csr was hacked into auth
|
||||
def run(args, config, plugins): # pylint: disable=too-many-branches,too-many-locals
|
||||
"""Obtain a certificate and install."""
|
||||
try:
|
||||
installer, authenticator = choose_configurator_plugins(args, config, plugins, "run")
|
||||
except PluginSelectionError, e:
|
||||
except errors.PluginSelectionError, e:
|
||||
return e.message
|
||||
|
||||
domains = _find_domains(args, installer)
|
||||
@@ -447,7 +454,7 @@ def run(args, config, plugins): # pylint: disable=too-many-branches,too-many-lo
|
||||
# TODO: Handle errors from _init_le_client?
|
||||
le_client = _init_le_client(args, config, authenticator, installer)
|
||||
|
||||
lineage = _auth_from_domains(le_client, config, domains, plugins)
|
||||
lineage = _auth_from_domains(le_client, config, domains)
|
||||
|
||||
le_client.deploy_certificate(
|
||||
domains, lineage.privkey, lineage.cert,
|
||||
@@ -461,7 +468,7 @@ def run(args, config, plugins): # pylint: disable=too-many-branches,too-many-lo
|
||||
display_ops.success_renewal(domains)
|
||||
|
||||
|
||||
def obtaincert(args, config, plugins):
|
||||
def obtain_cert(args, config, plugins):
|
||||
"""Authenticate & obtain cert, but do not install it."""
|
||||
|
||||
if args.domains is not None and args.csr is not None:
|
||||
@@ -472,7 +479,7 @@ def obtaincert(args, config, plugins):
|
||||
try:
|
||||
# installers are used in auth mode to determine domain names
|
||||
installer, authenticator = choose_configurator_plugins(args, config, plugins, "certonly")
|
||||
except PluginSelectionError, e:
|
||||
except errors.PluginSelectionError, e:
|
||||
return e.message
|
||||
|
||||
# TODO: Handle errors from _init_le_client?
|
||||
@@ -487,7 +494,7 @@ def obtaincert(args, config, plugins):
|
||||
_report_new_cert(cert_path, cert_fullchain)
|
||||
else:
|
||||
domains = _find_domains(args, installer)
|
||||
_auth_from_domains(le_client, config, domains, plugins)
|
||||
_auth_from_domains(le_client, config, domains)
|
||||
|
||||
|
||||
def install(args, config, plugins):
|
||||
@@ -497,7 +504,7 @@ def install(args, config, plugins):
|
||||
try:
|
||||
installer, _ = choose_configurator_plugins(args, config,
|
||||
plugins, "install")
|
||||
except PluginSelectionError, e:
|
||||
except errors.PluginSelectionError, e:
|
||||
return e.message
|
||||
|
||||
domains = _find_domains(args, installer)
|
||||
@@ -512,18 +519,19 @@ def install(args, config, plugins):
|
||||
|
||||
def revoke(args, config, unused_plugins): # TODO: coop with renewal config
|
||||
"""Revoke a previously obtained certificate."""
|
||||
# For user-agent construction
|
||||
config.namespace.installer = config.namespace.authenticator = "none"
|
||||
if args.key_path is not None: # revocation by cert key
|
||||
logger.debug("Revoking %s using cert key %s",
|
||||
args.cert_path[0], args.key_path[0])
|
||||
acme = acme_client.Client(
|
||||
config.server, key=jose.JWK.load(args.key_path[1]))
|
||||
key = jose.JWK.load(args.key_path[1])
|
||||
else: # revocation by account key
|
||||
logger.debug("Revoking %s using Account Key", args.cert_path[0])
|
||||
acc, _ = _determine_account(args, config)
|
||||
# pylint: disable=protected-access
|
||||
acme = client._acme_from_config_key(config, acc.key)
|
||||
acme.revoke(jose.ComparableX509(crypto_util.pyopenssl_load_certificate(
|
||||
args.cert_path[1])[0]))
|
||||
key = acc.key
|
||||
acme = client.acme_from_config_key(config, key)
|
||||
cert = crypto_util.pyopenssl_load_certificate(args.cert_path[1])[0]
|
||||
acme.revoke(jose.ComparableX509(cert))
|
||||
|
||||
|
||||
def rollback(args, config, plugins):
|
||||
@@ -549,6 +557,7 @@ def plugins_cmd(args, config, plugins): # TODO: Use IDisplay rather than print
|
||||
logger.debug("Filtered plugins: %r", filtered)
|
||||
|
||||
if not args.init and not args.prepare:
|
||||
print str(filtered)
|
||||
return
|
||||
|
||||
filtered.init(config)
|
||||
@@ -556,26 +565,29 @@ def plugins_cmd(args, config, plugins): # TODO: Use IDisplay rather than print
|
||||
logger.debug("Verified plugins: %r", verified)
|
||||
|
||||
if not args.prepare:
|
||||
print str(verified)
|
||||
return
|
||||
|
||||
verified.prepare()
|
||||
available = verified.available()
|
||||
logger.debug("Prepared plugins: %s", available)
|
||||
print str(available)
|
||||
|
||||
|
||||
def read_file(filename, mode="rb"):
|
||||
"""Returns the given file's contents.
|
||||
|
||||
:param str filename: Filename
|
||||
:param str filename: path to file
|
||||
:param str mode: open mode (see `open`)
|
||||
|
||||
:returns: A tuple of filename and its contents
|
||||
:returns: absolute path of filename and its contents
|
||||
:rtype: tuple
|
||||
|
||||
:raises argparse.ArgumentTypeError: File does not exist or is not readable.
|
||||
|
||||
"""
|
||||
try:
|
||||
filename = os.path.abspath(filename)
|
||||
return filename, open(filename, mode).read()
|
||||
except IOError as exc:
|
||||
raise argparse.ArgumentTypeError(exc.strerror)
|
||||
@@ -621,7 +633,7 @@ class HelpfulArgumentParser(object):
|
||||
"""
|
||||
|
||||
# Maps verbs/subcommands to the functions that implement them
|
||||
VERBS = {"auth": obtaincert, "certonly": obtaincert,
|
||||
VERBS = {"auth": obtain_cert, "certonly": obtain_cert,
|
||||
"config_changes": config_changes, "everything": run,
|
||||
"install": install, "plugins": plugins_cmd,
|
||||
"revoke": revoke, "rollback": rollback, "run": run}
|
||||
@@ -668,8 +680,32 @@ class HelpfulArgumentParser(object):
|
||||
parsed_args = self.parser.parse_args(self.args)
|
||||
parsed_args.func = self.VERBS[self.verb]
|
||||
|
||||
parsed_args.domains = self._parse_domains(parsed_args.domains)
|
||||
return parsed_args
|
||||
|
||||
def _parse_domains(self, domains):
|
||||
"""Helper function for parse_args() that parses domains from a
|
||||
(possibly) comma separated list and returns list of unique domains.
|
||||
|
||||
:param domains: List of domain flags
|
||||
:type domains: `list` of `string`
|
||||
|
||||
:returns: List of unique domains
|
||||
:rtype: `list` of `string`
|
||||
|
||||
"""
|
||||
|
||||
uniqd = None
|
||||
|
||||
if domains:
|
||||
dlist = []
|
||||
for domain in domains:
|
||||
dlist.extend([d.strip() for d in domain.split(",")])
|
||||
# Make sure we don't have duplicates
|
||||
uniqd = [d for i, d in enumerate(dlist) if d not in dlist[:i]]
|
||||
|
||||
return uniqd
|
||||
|
||||
def determine_verb(self):
|
||||
"""Determines the verb/subcommand provided by the user.
|
||||
|
||||
@@ -811,7 +847,11 @@ def prepare_and_parse_args(plugins, args):
|
||||
# --domains is useful, because it can be stored in config
|
||||
#for subparser in parser_run, parser_auth, parser_install:
|
||||
# subparser.add_argument("domains", nargs="*", metavar="domain")
|
||||
helpful.add(None, "-d", "--domains", metavar="DOMAIN", action="append")
|
||||
helpful.add(None, "-d", "--domains", dest="domains",
|
||||
metavar="DOMAIN", action="append",
|
||||
help="Domain names to apply. For multiple domains you can use "
|
||||
"multiple -d flags or enter a comma separated list of domains"
|
||||
"as a parameter.")
|
||||
helpful.add(
|
||||
None, "--duplicate", dest="duplicate", action="store_true",
|
||||
help="Allow getting a certificate that duplicates an existing one")
|
||||
@@ -850,8 +890,9 @@ def prepare_and_parse_args(plugins, args):
|
||||
help=config_help("no_verify_ssl"),
|
||||
default=flag_default("no_verify_ssl"))
|
||||
helpful.add(
|
||||
"testing", "--dvsni-port", type=int, default=flag_default("dvsni_port"),
|
||||
help=config_help("dvsni_port"))
|
||||
"testing", "--tls-sni-01-port", type=int,
|
||||
default=flag_default("tls_sni_01_port"),
|
||||
help=config_help("tls_sni_01_port"))
|
||||
helpful.add("testing", "--http-01-port", dest="http01_port", type=int,
|
||||
help=config_help("http01_port"))
|
||||
|
||||
@@ -889,7 +930,6 @@ def prepare_and_parse_args(plugins, args):
|
||||
# parser (--help should display plugin-specific options last)
|
||||
_plugins_parsing(helpful, plugins)
|
||||
|
||||
|
||||
return helpful.parse_args()
|
||||
|
||||
|
||||
@@ -899,7 +939,13 @@ def _create_subparsers(helpful):
|
||||
helpful.add_group("revoke", description="Options for revocation of certs")
|
||||
helpful.add_group("rollback", description="Options for reverting config changes")
|
||||
helpful.add_group("plugins", description="Plugin options")
|
||||
|
||||
helpful.add(
|
||||
None, "--user-agent", default=None,
|
||||
help="Set a custom user agent string for the client. User agent strings allow "
|
||||
"the CA to collect high level statistics about success rates by OS and "
|
||||
"plugin. If you wish to hide your server OS version from the Let's "
|
||||
'Encrypt server, set this to "".'
|
||||
)
|
||||
helpful.add("certonly",
|
||||
"--csr", type=read_file,
|
||||
help="Path to a Certificate Signing Request (CSR) in DER"
|
||||
@@ -934,26 +980,29 @@ def _paths_parser(helpful):
|
||||
if verb in ("install", "revoke", "certonly"):
|
||||
section = verb
|
||||
if verb == "certonly":
|
||||
add(section, "--cert-path", default=flag_default("auth_cert_path"), help=cph)
|
||||
add(section, "--cert-path", type=os.path.abspath,
|
||||
default=flag_default("auth_cert_path"), help=cph)
|
||||
elif verb == "revoke":
|
||||
add(section, "--cert-path", type=read_file, required=True, help=cph)
|
||||
else:
|
||||
add(section, "--cert-path", help=cph, required=(verb == "install"))
|
||||
add(section, "--cert-path", type=os.path.abspath,
|
||||
help=cph, required=(verb == "install"))
|
||||
|
||||
section = "paths"
|
||||
if verb in ("install", "revoke"):
|
||||
section = verb
|
||||
# revoke --key-path reads a file, install --key-path takes a string
|
||||
add(section, "--key-path", type=((verb == "revoke" and read_file) or str),
|
||||
required=(verb == "install"),
|
||||
help="Path to private key for cert creation or revocation (if account key is missing)")
|
||||
add(section, "--key-path", required=(verb == "install"),
|
||||
type=((verb == "revoke" and read_file) or os.path.abspath),
|
||||
help="Path to private key for cert installation "
|
||||
"or revocation (if account key is missing)")
|
||||
|
||||
default_cp = None
|
||||
if verb == "certonly":
|
||||
default_cp = flag_default("auth_chain_path")
|
||||
add("paths", "--fullchain-path", default=default_cp,
|
||||
add("paths", "--fullchain-path", default=default_cp, type=os.path.abspath,
|
||||
help="Accompanying path to a full certificate chain (cert plus chain).")
|
||||
add("paths", "--chain-path", default=default_cp,
|
||||
add("paths", "--chain-path", default=default_cp, type=os.path.abspath,
|
||||
help="Accompanying path to a certificate chain.")
|
||||
add("paths", "--config-dir", default=flag_default("config_dir"),
|
||||
help=config_help("config_dir"))
|
||||
@@ -1069,7 +1118,7 @@ def _handle_exception(exc_type, exc_value, trace, args):
|
||||
sys.exit("".join(
|
||||
traceback.format_exception(exc_type, exc_value, trace)))
|
||||
|
||||
if issubclass(exc_type, Error):
|
||||
if issubclass(exc_type, errors.Error):
|
||||
sys.exit(exc_value)
|
||||
else:
|
||||
# Tell the user a bit about what happened, without overwhelming
|
||||
@@ -1133,7 +1182,7 @@ def main(cli_args=sys.argv[1:]):
|
||||
disclaimer = pkg_resources.resource_string("letsencrypt", "DISCLAIMER")
|
||||
if not zope.component.getUtility(interfaces.IDisplay).yesno(
|
||||
disclaimer, "Agree", "Cancel"):
|
||||
raise Error("Must agree to TOS")
|
||||
raise errors.Error("Must agree to TOS")
|
||||
|
||||
if not os.geteuid() == 0:
|
||||
logger.warning(
|
||||
@@ -1148,7 +1197,6 @@ def main(cli_args=sys.argv[1:]):
|
||||
|
||||
return args.func(args, config, plugins)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
err_string = main()
|
||||
if err_string:
|
||||
|
||||
@@ -5,11 +5,14 @@ import os
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.hazmat.primitives.asymmetric import rsa
|
||||
import OpenSSL
|
||||
import zope.component
|
||||
|
||||
from acme import client as acme_client
|
||||
from acme import jose
|
||||
from acme import messages
|
||||
|
||||
import letsencrypt
|
||||
|
||||
from letsencrypt import account
|
||||
from letsencrypt import auth_handler
|
||||
from letsencrypt import configuration
|
||||
@@ -18,6 +21,7 @@ from letsencrypt import continuity_auth
|
||||
from letsencrypt import crypto_util
|
||||
from letsencrypt import errors
|
||||
from letsencrypt import error_handler
|
||||
from letsencrypt import interfaces
|
||||
from letsencrypt import le_util
|
||||
from letsencrypt import reverter
|
||||
from letsencrypt import storage
|
||||
@@ -29,10 +33,30 @@ from letsencrypt.display import enhancements
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _acme_from_config_key(config, key):
|
||||
def acme_from_config_key(config, key):
|
||||
"Wrangle ACME client construction"
|
||||
# TODO: Allow for other alg types besides RS256
|
||||
return acme_client.Client(directory=config.server, key=key,
|
||||
verify_ssl=(not config.no_verify_ssl))
|
||||
net = acme_client.ClientNetwork(key, verify_ssl=(not config.no_verify_ssl),
|
||||
user_agent=_determine_user_agent(config))
|
||||
return acme_client.Client(config.server, key=key, net=net)
|
||||
|
||||
|
||||
def _determine_user_agent(config):
|
||||
"""
|
||||
Set a user_agent string in the config based on the choice of plugins.
|
||||
(this wasn't knowable at construction time)
|
||||
|
||||
:returns: the client's User-Agent string
|
||||
:rtype: `str`
|
||||
"""
|
||||
|
||||
if config.user_agent is None:
|
||||
ua = "LetsEncryptPythonClient/{0} ({1}) Authenticator/{2} Installer/{3}"
|
||||
ua = ua.format(letsencrypt.__version__, " ".join(le_util.get_os_info()),
|
||||
config.authenticator, config.installer)
|
||||
else:
|
||||
ua = config.user_agent
|
||||
return ua
|
||||
|
||||
|
||||
def register(config, account_storage, tos_cb=None):
|
||||
@@ -84,7 +108,7 @@ def register(config, account_storage, tos_cb=None):
|
||||
public_exponent=65537,
|
||||
key_size=config.rsa_key_size,
|
||||
backend=default_backend())))
|
||||
acme = _acme_from_config_key(config, key)
|
||||
acme = acme_from_config_key(config, key)
|
||||
# TODO: add phone?
|
||||
regr = acme.register(messages.NewRegistration.from_data(email=config.email))
|
||||
|
||||
@@ -98,6 +122,7 @@ def register(config, account_storage, tos_cb=None):
|
||||
acc = account.Account(regr, key)
|
||||
account.report_new_account(acc, config)
|
||||
account_storage.save(acc)
|
||||
|
||||
return acc, acme
|
||||
|
||||
|
||||
@@ -126,7 +151,7 @@ class Client(object):
|
||||
|
||||
# Initialize ACME if account is provided
|
||||
if acme is None and self.account is not None:
|
||||
acme = _acme_from_config_key(config, self.account.key)
|
||||
acme = acme_from_config_key(config, self.account.key)
|
||||
self.acme = acme
|
||||
|
||||
# TODO: Check if self.config.enroll_autorenew is None. If
|
||||
@@ -211,7 +236,7 @@ class Client(object):
|
||||
|
||||
return self._obtain_certificate(domains, csr) + (key, csr)
|
||||
|
||||
def obtain_and_enroll_certificate(self, domains, plugins):
|
||||
def obtain_and_enroll_certificate(self, domains):
|
||||
"""Obtain and enroll certificate.
|
||||
|
||||
Get a new certificate for the specified domains using the specified
|
||||
@@ -228,13 +253,6 @@ class Client(object):
|
||||
"""
|
||||
certr, chain, key, _ = self.obtain_certificate(domains)
|
||||
|
||||
# TODO: remove this dirty hack
|
||||
self.config.namespace.authenticator = plugins.find_init(
|
||||
self.dv_auth).name
|
||||
if self.installer is not None:
|
||||
self.config.namespace.installer = plugins.find_init(
|
||||
self.installer).name
|
||||
|
||||
# XXX: We clearly need a more general and correct way of getting
|
||||
# options into the configobj for the RenewableCert instance.
|
||||
# This is a quick-and-dirty way to do it to allow integration
|
||||
@@ -330,26 +348,12 @@ class Client(object):
|
||||
|
||||
self.installer.save("Deployed Let's Encrypt Certificate")
|
||||
|
||||
# sites may have been enabled / final cleanup
|
||||
with error_handler.ErrorHandler(self._rollback_and_restart):
|
||||
msg = ("We were unable to install your certificate, "
|
||||
"however, we successfully restored your "
|
||||
"server to its prior configuration.")
|
||||
with error_handler.ErrorHandler(self._rollback_and_restart, msg):
|
||||
# sites may have been enabled / final cleanup
|
||||
self.installer.restart()
|
||||
|
||||
def _rollback_and_restart(self):
|
||||
"""Rollback the most recent checkpoint and restart the webserver"""
|
||||
logger.critical("Rolling back to previous server configuration...")
|
||||
try:
|
||||
self.installer.rollback_checkpoints()
|
||||
self.installer.restart()
|
||||
except:
|
||||
# TODO: suggest letshelp-letsencypt here
|
||||
logger.critical("Failure to rollback config "
|
||||
"changes and restart your server")
|
||||
logger.critical("Please submit a bug report to "
|
||||
"https://github.com/letsencrypt/letsencrypt")
|
||||
raise
|
||||
logger.critical("Rollback successful; your server has "
|
||||
"been restarted with your old configuration")
|
||||
|
||||
def enhance_config(self, domains, config=None):
|
||||
"""Enhance the configuration.
|
||||
|
||||
@@ -387,7 +391,8 @@ class Client(object):
|
||||
"Upgrade-Insecure-Requests")
|
||||
|
||||
if redirect or hsts or uir:
|
||||
self.installer.restart()
|
||||
with error_handler.ErrorHandler(self._rollback_and_restart, msg):
|
||||
self.installer.restart()
|
||||
|
||||
def apply_enhancement(self, domains, enhancement, options=None):
|
||||
"""Applies an enhacement on all domains.
|
||||
@@ -403,7 +408,9 @@ class Client(object):
|
||||
:type str
|
||||
|
||||
"""
|
||||
with error_handler.ErrorHandler(self.installer.recovery_routine):
|
||||
msg = ("We were unable to set up a redirect for your server, "
|
||||
"however, we successfully installed your certificate.")
|
||||
with error_handler.ErrorHandler(self._recovery_routine_with_msg, msg):
|
||||
for dom in domains:
|
||||
try:
|
||||
self.installer.enhance(dom, enhancement, options)
|
||||
@@ -414,6 +421,37 @@ class Client(object):
|
||||
|
||||
self.installer.save("Add enhancement %s" % (enhancement))
|
||||
|
||||
def _recovery_routine_with_msg(self, success_msg):
|
||||
"""Calls the installer's recovery routine and prints success_msg
|
||||
|
||||
:param str success_msg: message to show on successful recovery
|
||||
|
||||
"""
|
||||
self.installer.recovery_routine()
|
||||
reporter = zope.component.getUtility(interfaces.IReporter)
|
||||
reporter.add_message(success_msg, reporter.HIGH_PRIORITY)
|
||||
|
||||
def _rollback_and_restart(self, success_msg):
|
||||
"""Rollback the most recent checkpoint and restart the webserver
|
||||
|
||||
:param str success_msg: message to show on successful rollback
|
||||
|
||||
"""
|
||||
logger.critical("Rolling back to previous server configuration...")
|
||||
reporter = zope.component.getUtility(interfaces.IReporter)
|
||||
try:
|
||||
self.installer.rollback_checkpoints()
|
||||
self.installer.restart()
|
||||
except:
|
||||
# TODO: suggest letshelp-letsencypt here
|
||||
reporter.add_message(
|
||||
"An error occured and we failed to restore your config and "
|
||||
"restart your server. Please submit a bug report to "
|
||||
"https://github.com/letsencrypt/letsencrypt",
|
||||
reporter.HIGH_PRIORITY)
|
||||
raise
|
||||
reporter.add_message(success_msg, reporter.HIGH_PRIORITY)
|
||||
|
||||
|
||||
def validate_key_csr(privkey, csr=None):
|
||||
"""Validate Key and CSR files.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Let's Encrypt user-supplied configuration."""
|
||||
import os
|
||||
import urlparse
|
||||
import re
|
||||
|
||||
import zope.interface
|
||||
|
||||
@@ -37,10 +38,12 @@ class NamespaceConfig(object):
|
||||
def __init__(self, namespace):
|
||||
self.namespace = namespace
|
||||
|
||||
if self.http01_port == self.dvsni_port:
|
||||
raise errors.Error(
|
||||
"Trying to run http-01 and DVSNI "
|
||||
"on the same port ({0})".format(self.dvsni_port))
|
||||
self.namespace.config_dir = os.path.abspath(self.namespace.config_dir)
|
||||
self.namespace.work_dir = os.path.abspath(self.namespace.work_dir)
|
||||
self.namespace.logs_dir = os.path.abspath(self.namespace.logs_dir)
|
||||
|
||||
# Check command line parameters sanity, and error out in case of problem.
|
||||
check_config_sanity(self)
|
||||
|
||||
def __getattr__(self, name):
|
||||
return getattr(self.namespace, name)
|
||||
@@ -111,3 +114,49 @@ class RenewerConfiguration(object):
|
||||
def renewer_config_file(self): # pylint: disable=missing-docstring
|
||||
return os.path.join(
|
||||
self.namespace.config_dir, constants.RENEWER_CONFIG_FILENAME)
|
||||
|
||||
|
||||
def check_config_sanity(config):
|
||||
"""Validate command line options and display error message if
|
||||
requirements are not met.
|
||||
|
||||
:param config: IConfig instance holding user configuration
|
||||
:type args: :class:`letsencrypt.interfaces.IConfig`
|
||||
|
||||
"""
|
||||
# Port check
|
||||
if config.http01_port == config.tls_sni_01_port:
|
||||
raise errors.ConfigurationError(
|
||||
"Trying to run http-01 and tls-sni-01 "
|
||||
"on the same port ({0})".format(config.tls_sni_01_port))
|
||||
|
||||
# Domain checks
|
||||
if config.namespace.domains is not None:
|
||||
_check_config_domain_sanity(config.namespace.domains)
|
||||
|
||||
|
||||
def _check_config_domain_sanity(domains):
|
||||
"""Helper method for check_config_sanity which validates
|
||||
domain flag values and errors out if the requirements are not met.
|
||||
|
||||
:param domains: List of domains
|
||||
:type domains: `list` of `string`
|
||||
:raises ConfigurationError: for invalid domains and cases where Let's
|
||||
Encrypt currently will not issue certificates
|
||||
|
||||
"""
|
||||
# Check if there's a wildcard domain
|
||||
if any(d.startswith("*.") for d in domains):
|
||||
raise errors.ConfigurationError(
|
||||
"Wildcard domains are not supported")
|
||||
# Punycode
|
||||
if any("xn--" in d for d in domains):
|
||||
raise errors.ConfigurationError(
|
||||
"Punycode domains are not supported")
|
||||
# FQDN checks from
|
||||
# http://www.mkyong.com/regular-expressions/domain-name-regular-expression-example/
|
||||
# Characters used, domain parts < 63 chars, tld > 1 < 7 chars
|
||||
# first and last char is not "-"
|
||||
fqdn = re.compile("^((?!-)[A-Za-z0-9-]{1,63}(?<!-)\\.)+[A-Za-z]{2,6}$")
|
||||
if any(True for d in domains if not fqdn.match(d)):
|
||||
raise errors.ConfigurationError("Requested domain is not a FQDN")
|
||||
|
||||
@@ -23,7 +23,7 @@ CLI_DEFAULTS = dict(
|
||||
work_dir="/var/lib/letsencrypt",
|
||||
logs_dir="/var/log/letsencrypt",
|
||||
no_verify_ssl=False,
|
||||
dvsni_port=challenges.DVSNI.PORT,
|
||||
tls_sni_01_port=challenges.TLSSNI01Response.PORT,
|
||||
|
||||
auth_cert_path="./cert.pem",
|
||||
auth_chain_path="./chain.pem",
|
||||
@@ -41,7 +41,7 @@ RENEWER_DEFAULTS = dict(
|
||||
|
||||
|
||||
EXCLUSIVE_CHALLENGES = frozenset([frozenset([
|
||||
challenges.DVSNI, challenges.HTTP01])])
|
||||
challenges.TLSSNI01, challenges.HTTP01])])
|
||||
"""Mutually exclusive challenges."""
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"""Registers functions to be called if an exception or signal occurs."""
|
||||
import functools
|
||||
import logging
|
||||
import os
|
||||
import signal
|
||||
@@ -40,11 +41,11 @@ class ErrorHandler(object):
|
||||
to be called again by the next signal handler.
|
||||
|
||||
"""
|
||||
def __init__(self, func=None):
|
||||
def __init__(self, func=None, *args, **kwargs):
|
||||
self.funcs = []
|
||||
self.prev_handlers = {}
|
||||
if func is not None:
|
||||
self.register(func)
|
||||
self.register(func, *args, **kwargs)
|
||||
|
||||
def __enter__(self):
|
||||
self.set_signal_handlers()
|
||||
@@ -57,9 +58,13 @@ class ErrorHandler(object):
|
||||
self.call_registered()
|
||||
self.reset_signal_handlers()
|
||||
|
||||
def register(self, func):
|
||||
"""Registers func to be called if an error occurs."""
|
||||
self.funcs.append(func)
|
||||
def register(self, func, *args, **kwargs):
|
||||
"""Sets func to be called with *args and **kwargs during cleanup
|
||||
|
||||
:param function func: function to be called in case of an error
|
||||
|
||||
"""
|
||||
self.funcs.append(functools.partial(func, *args, **kwargs))
|
||||
|
||||
def call_registered(self):
|
||||
"""Calls all registered functions"""
|
||||
|
||||
@@ -57,8 +57,8 @@ class DvAuthError(AuthorizationError):
|
||||
|
||||
|
||||
# Authenticator - Challenge specific errors
|
||||
class DvsniError(DvAuthError):
|
||||
"""Let's Encrypt DVSNI error."""
|
||||
class TLSSNI01Error(DvAuthError):
|
||||
"""Let's Encrypt TLSSNI01 error."""
|
||||
|
||||
|
||||
# Plugin Errors
|
||||
@@ -94,3 +94,7 @@ class StandaloneBindError(Error):
|
||||
"Problem binding to port {0}: {1}".format(port, socket_error))
|
||||
self.socket_error = socket_error
|
||||
self.port = port
|
||||
|
||||
|
||||
class ConfigurationError(Error):
|
||||
"""Configuration sanity error."""
|
||||
|
||||
@@ -219,8 +219,8 @@ class IConfig(zope.interface.Interface):
|
||||
|
||||
no_verify_ssl = zope.interface.Attribute(
|
||||
"Disable SSL certificate verification.")
|
||||
dvsni_port = zope.interface.Attribute(
|
||||
"Port number to perform DVSNI challenge. "
|
||||
tls_sni_01_port = zope.interface.Attribute(
|
||||
"Port number to perform tls-sni-01 challenge. "
|
||||
"Boulder in testing mode defaults to 5001.")
|
||||
|
||||
http01_port = zope.interface.Attribute(
|
||||
@@ -298,7 +298,8 @@ class IInstaller(IPlugin):
|
||||
|
||||
Both title and temporary are needed because a save may be
|
||||
intended to be permanent, but the save is not ready to be a full
|
||||
checkpoint
|
||||
checkpoint. If an exception is raised, it is assumed a new
|
||||
checkpoint was not created.
|
||||
|
||||
: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
|
||||
|
||||
@@ -3,6 +3,7 @@ import collections
|
||||
import errno
|
||||
import logging
|
||||
import os
|
||||
import platform
|
||||
import re
|
||||
import subprocess
|
||||
import stat
|
||||
@@ -202,6 +203,45 @@ def safely_remove(path):
|
||||
raise
|
||||
|
||||
|
||||
def get_os_info():
|
||||
"""
|
||||
Get Operating System type/distribution and major version
|
||||
|
||||
:returns: (os_name, os_version)
|
||||
:rtype: `tuple` of `str`
|
||||
"""
|
||||
info = platform.system_alias(
|
||||
platform.system(),
|
||||
platform.release(),
|
||||
platform.version()
|
||||
)
|
||||
os_type, os_ver, _ = info
|
||||
os_type = os_type.lower()
|
||||
if os_type.startswith('linux'):
|
||||
info = platform.linux_distribution()
|
||||
# On arch, platform.linux_distribution() is reportedly ('','',''),
|
||||
# so handle it defensively
|
||||
if info[0]:
|
||||
os_type = info[0]
|
||||
if info[1]:
|
||||
os_ver = info[1]
|
||||
elif os_type.startswith('darwin'):
|
||||
os_ver = subprocess.Popen(
|
||||
["sw_vers", "-productVersion"],
|
||||
stdout=subprocess.PIPE
|
||||
).communicate()[0]
|
||||
os_ver = os_ver.partition(".")[0]
|
||||
elif os_type.startswith('freebsd'):
|
||||
# eg "9.3-RC3-p1"
|
||||
os_ver = os_ver.partition("-")[0]
|
||||
os_ver = os_ver.partition(".")[0]
|
||||
elif platform.win32_ver()[1]:
|
||||
os_ver = platform.win32_ver()[1]
|
||||
else:
|
||||
# Cases known to fall here: Cygwin python
|
||||
os_ver = ''
|
||||
return os_type, os_ver
|
||||
|
||||
# Just make sure we don't get pwned... Make sure that it also doesn't
|
||||
# start with a period or have two consecutive periods <- this needs to
|
||||
# be done in addition to the regex
|
||||
|
||||
@@ -135,22 +135,22 @@ class Addr(object):
|
||||
return self.__class__((self.tup[0], port))
|
||||
|
||||
|
||||
class Dvsni(object):
|
||||
"""Class that perform DVSNI challenges."""
|
||||
class TLSSNI01(object):
|
||||
"""Class that performs tls-sni-01 challenges."""
|
||||
|
||||
def __init__(self, configurator):
|
||||
self.configurator = configurator
|
||||
self.achalls = []
|
||||
self.indices = []
|
||||
self.challenge_conf = os.path.join(
|
||||
configurator.config.config_dir, "le_dvsni_cert_challenge.conf")
|
||||
configurator.config.config_dir, "le_tls_sni_01_cert_challenge.conf")
|
||||
# self.completed = 0
|
||||
|
||||
def add_chall(self, achall, idx=None):
|
||||
"""Add challenge to DVSNI object to perform at once.
|
||||
"""Add challenge to TLSSNI01 object to perform at once.
|
||||
|
||||
:param achall: Annotated DVSNI challenge.
|
||||
:type achall: :class:`letsencrypt.achallenges.DVSNI`
|
||||
:param .KeyAuthorizationAnnotatedChallenge achall: Annotated
|
||||
TLSSNI01 challenge.
|
||||
|
||||
:param int idx: index to challenge in a larger array
|
||||
|
||||
@@ -162,8 +162,8 @@ class Dvsni(object):
|
||||
def get_cert_path(self, achall):
|
||||
"""Returns standardized name for challenge certificate.
|
||||
|
||||
:param achall: Annotated DVSNI challenge.
|
||||
:type achall: :class:`letsencrypt.achallenges.DVSNI`
|
||||
:param .KeyAuthorizationAnnotatedChallenge achall: Annotated
|
||||
tls-sni-01 challenge.
|
||||
|
||||
:returns: certificate file name
|
||||
:rtype: str
|
||||
@@ -177,7 +177,7 @@ class Dvsni(object):
|
||||
return os.path.join(self.configurator.config.work_dir,
|
||||
achall.chall.encode("token") + '.pem')
|
||||
|
||||
def _setup_challenge_cert(self, achall, s=None):
|
||||
def _setup_challenge_cert(self, achall, cert_key=None):
|
||||
|
||||
"""Generate and write out challenge certificate."""
|
||||
cert_path = self.get_cert_path(achall)
|
||||
@@ -186,7 +186,8 @@ class Dvsni(object):
|
||||
self.configurator.reverter.register_file_creation(True, key_path)
|
||||
self.configurator.reverter.register_file_creation(True, cert_path)
|
||||
|
||||
response, cert, key = achall.gen_cert_and_response(s)
|
||||
response, (cert, key) = achall.response_and_validation(
|
||||
cert_key=cert_key)
|
||||
cert_pem = OpenSSL.crypto.dump_certificate(
|
||||
OpenSSL.crypto.FILETYPE_PEM, cert)
|
||||
key_pem = OpenSSL.crypto.dump_privatekey(
|
||||
|
||||
@@ -115,24 +115,24 @@ class AddrTest(unittest.TestCase):
|
||||
self.assertEqual(set_a, set_b)
|
||||
|
||||
|
||||
class DvsniTest(unittest.TestCase):
|
||||
"""Tests for letsencrypt.plugins.common.DvsniTest."""
|
||||
class TLSSNI01Test(unittest.TestCase):
|
||||
"""Tests for letsencrypt.plugins.common.TLSSNI01."""
|
||||
|
||||
auth_key = jose.JWKRSA.load(test_util.load_vector("rsa512_key.pem"))
|
||||
achalls = [
|
||||
achallenges.DVSNI(
|
||||
achallenges.KeyAuthorizationAnnotatedChallenge(
|
||||
challb=acme_util.chall_to_challb(
|
||||
challenges.DVSNI(token=b'dvsni1'), "pending"),
|
||||
challenges.TLSSNI01(token=b'token1'), "pending"),
|
||||
domain="encryption-example.demo", account_key=auth_key),
|
||||
achallenges.DVSNI(
|
||||
achallenges.KeyAuthorizationAnnotatedChallenge(
|
||||
challb=acme_util.chall_to_challb(
|
||||
challenges.DVSNI(token=b'dvsni2'), "pending"),
|
||||
challenges.TLSSNI01(token=b'token2'), "pending"),
|
||||
domain="letsencrypt.demo", account_key=auth_key),
|
||||
]
|
||||
|
||||
def setUp(self):
|
||||
from letsencrypt.plugins.common import Dvsni
|
||||
self.sni = Dvsni(configurator=mock.MagicMock())
|
||||
from letsencrypt.plugins.common import TLSSNI01
|
||||
self.sni = TLSSNI01(configurator=mock.MagicMock())
|
||||
|
||||
def test_add_chall(self):
|
||||
self.sni.add_chall(self.achalls[0], 0)
|
||||
@@ -146,11 +146,11 @@ class DvsniTest(unittest.TestCase):
|
||||
# http://www.voidspace.org.uk/python/mock/helpers.html#mock.mock_open
|
||||
mock_open, mock_safe_open = mock.mock_open(), mock.mock_open()
|
||||
|
||||
response = challenges.DVSNIResponse(validation=mock.Mock())
|
||||
response = challenges.TLSSNI01Response()
|
||||
achall = mock.MagicMock()
|
||||
key = test_util.load_pyopenssl_private_key("rsa512_key.pem")
|
||||
achall.gen_cert_and_response.return_value = (
|
||||
response, test_util.load_cert("cert.pem"), key)
|
||||
achall.response_and_validation.return_value = (
|
||||
response, (test_util.load_cert("cert.pem"), key))
|
||||
|
||||
with mock.patch("letsencrypt.plugins.common.open",
|
||||
mock_open, create=True):
|
||||
|
||||
@@ -51,8 +51,8 @@ class PluginEntryPointTest(unittest.TestCase):
|
||||
|
||||
def test_description(self):
|
||||
self.assertEqual(
|
||||
"Automatically use a temporary webserver",
|
||||
self.plugin_ep.description)
|
||||
"Automatically use a temporary webserver",
|
||||
self.plugin_ep.description)
|
||||
|
||||
def test_description_with_name(self):
|
||||
self.plugin_ep.plugin_cls = mock.MagicMock(description="Desc")
|
||||
|
||||
@@ -31,7 +31,7 @@ class Authenticator(common.Plugin):
|
||||
run as a privileged process. Alternatively shows instructions on how
|
||||
to use Python's built-in HTTP server.
|
||||
|
||||
.. todo:: Support for `~.challenges.DVSNI`.
|
||||
.. todo:: Support for `~.challenges.TLSSNI01`.
|
||||
|
||||
"""
|
||||
zope.interface.implements(interfaces.IAuthenticator)
|
||||
|
||||
@@ -11,7 +11,6 @@ import six
|
||||
import zope.interface
|
||||
|
||||
from acme import challenges
|
||||
from acme import crypto_util as acme_crypto_util
|
||||
from acme import standalone as acme_standalone
|
||||
|
||||
from letsencrypt import errors
|
||||
@@ -28,9 +27,9 @@ class ServerManager(object):
|
||||
|
||||
Manager for `ACMEServer` and `ACMETLSServer` instances.
|
||||
|
||||
`certs` and `simple_http_resources` correspond to
|
||||
`certs` and `http_01_resources` correspond to
|
||||
`acme.crypto_util.SSLSocket.certs` and
|
||||
`acme.crypto_util.SSLSocket.simple_http_resources` respectively. All
|
||||
`acme.crypto_util.SSLSocket.http_01_resources` respectively. All
|
||||
created servers share the same certificates and resources, so if
|
||||
you're running both TLS and non-TLS instances, HTTP01 handlers
|
||||
will serve the same URLs!
|
||||
@@ -38,10 +37,10 @@ class ServerManager(object):
|
||||
"""
|
||||
_Instance = collections.namedtuple("_Instance", "server thread")
|
||||
|
||||
def __init__(self, certs, simple_http_resources):
|
||||
def __init__(self, certs, http_01_resources):
|
||||
self._instances = {}
|
||||
self.certs = certs
|
||||
self.simple_http_resources = simple_http_resources
|
||||
self.http_01_resources = http_01_resources
|
||||
|
||||
def run(self, port, challenge_type):
|
||||
"""Run ACME server on specified ``port``.
|
||||
@@ -51,23 +50,23 @@ class ServerManager(object):
|
||||
|
||||
:param int port: Port to run the server on.
|
||||
:param challenge_type: Subclass of `acme.challenges.Challenge`,
|
||||
either `acme.challenge.HTTP01` or `acme.challenges.DVSNI`.
|
||||
either `acme.challenge.HTTP01` or `acme.challenges.TLSSNI01`.
|
||||
|
||||
:returns: Server instance.
|
||||
:rtype: ACMEServerMixin
|
||||
|
||||
"""
|
||||
assert challenge_type in (challenges.DVSNI, challenges.HTTP01)
|
||||
assert challenge_type in (challenges.TLSSNI01, challenges.HTTP01)
|
||||
if port in self._instances:
|
||||
return self._instances[port].server
|
||||
|
||||
address = ("", port)
|
||||
try:
|
||||
if challenge_type is challenges.DVSNI:
|
||||
server = acme_standalone.DVSNIServer(address, self.certs)
|
||||
if challenge_type is challenges.TLSSNI01:
|
||||
server = acme_standalone.TLSSNI01Server(address, self.certs)
|
||||
else: # challenges.HTTP01
|
||||
server = acme_standalone.HTTP01Server(
|
||||
address, self.simple_http_resources)
|
||||
address, self.http_01_resources)
|
||||
except socket.error as error:
|
||||
raise errors.StandaloneBindError(error, port)
|
||||
|
||||
@@ -109,7 +108,7 @@ class ServerManager(object):
|
||||
in six.iteritems(self._instances))
|
||||
|
||||
|
||||
SUPPORTED_CHALLENGES = set([challenges.DVSNI, challenges.HTTP01])
|
||||
SUPPORTED_CHALLENGES = set([challenges.TLSSNI01, challenges.HTTP01])
|
||||
|
||||
|
||||
def supported_challenges_validator(data):
|
||||
@@ -138,7 +137,7 @@ class Authenticator(common.Plugin):
|
||||
"""Standalone Authenticator.
|
||||
|
||||
This authenticator creates its own ephemeral TCP listener on the
|
||||
necessary port in order to respond to incoming DVSNI and HTTP01
|
||||
necessary port in order to respond to incoming tls-sni-01 and http-01
|
||||
challenges from the certificate authority. Therefore, it does not
|
||||
rely on any existing server program.
|
||||
"""
|
||||
@@ -150,12 +149,9 @@ class Authenticator(common.Plugin):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(Authenticator, self).__init__(*args, **kwargs)
|
||||
|
||||
# one self-signed key for all DVSNI and HTTP01 certificates
|
||||
# one self-signed key for all tls-sni-01 certificates
|
||||
self.key = OpenSSL.crypto.PKey()
|
||||
self.key.generate_key(OpenSSL.crypto.TYPE_RSA, bits=2048)
|
||||
# TODO: generate only when the first HTTP01 challenge is solved
|
||||
self.simple_http_cert = acme_crypto_util.gen_ss_cert(
|
||||
self.key, domains=["temp server"])
|
||||
|
||||
self.served = collections.defaultdict(set)
|
||||
|
||||
@@ -164,9 +160,9 @@ class Authenticator(common.Plugin):
|
||||
# GIL, the operations are safe, c.f.
|
||||
# https://docs.python.org/2/faq/library.html#what-kinds-of-global-value-mutation-are-thread-safe
|
||||
self.certs = {}
|
||||
self.simple_http_resources = set()
|
||||
self.http_01_resources = set()
|
||||
|
||||
self.servers = ServerManager(self.certs, self.simple_http_resources)
|
||||
self.servers = ServerManager(self.certs, self.http_01_resources)
|
||||
|
||||
@classmethod
|
||||
def add_parser_arguments(cls, add):
|
||||
@@ -186,15 +182,16 @@ class Authenticator(common.Plugin):
|
||||
necessary_ports = set()
|
||||
if challenges.HTTP01 in self.supported_challenges:
|
||||
necessary_ports.add(self.config.http01_port)
|
||||
if challenges.DVSNI in self.supported_challenges:
|
||||
necessary_ports.add(self.config.dvsni_port)
|
||||
if challenges.TLSSNI01 in self.supported_challenges:
|
||||
necessary_ports.add(self.config.tls_sni_01_port)
|
||||
return necessary_ports
|
||||
|
||||
def more_info(self): # pylint: disable=missing-docstring
|
||||
return("This authenticator creates its own ephemeral TCP listener "
|
||||
"on the necessary port in order to respond to incoming DVSNI "
|
||||
"and HTTP01 challenges from the certificate authority. "
|
||||
"Therefore, it does not rely on any existing server program.")
|
||||
"on the necessary port in order to respond to incoming "
|
||||
"tls-sni-01 and http-01 challenges from the certificate "
|
||||
"authority. Therefore, it does not rely on any existing "
|
||||
"server program.")
|
||||
|
||||
def prepare(self): # pylint: disable=missing-docstring
|
||||
pass
|
||||
@@ -240,17 +237,16 @@ class Authenticator(common.Plugin):
|
||||
server = self.servers.run(
|
||||
self.config.http01_port, challenges.HTTP01)
|
||||
response, validation = achall.response_and_validation()
|
||||
self.simple_http_resources.add(
|
||||
self.http_01_resources.add(
|
||||
acme_standalone.HTTP01RequestHandler.HTTP01Resource(
|
||||
chall=achall.chall, response=response,
|
||||
validation=validation))
|
||||
cert = self.simple_http_cert
|
||||
domain = achall.domain
|
||||
else: # DVSNI
|
||||
server = self.servers.run(self.config.dvsni_port, challenges.DVSNI)
|
||||
response, cert, _ = achall.gen_cert_and_response(self.key)
|
||||
domain = response.z_domain
|
||||
self.certs[domain] = (self.key, cert)
|
||||
else: # tls-sni-01
|
||||
server = self.servers.run(
|
||||
self.config.tls_sni_01_port, challenges.TLSSNI01)
|
||||
response, (cert, _) = achall.response_and_validation(
|
||||
cert_key=self.key)
|
||||
self.certs[response.z_domain] = (self.key, cert)
|
||||
self.served[server].add(achall)
|
||||
responses.append(response)
|
||||
|
||||
|
||||
@@ -24,13 +24,13 @@ class ServerManagerTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
from letsencrypt.plugins.standalone import ServerManager
|
||||
self.certs = {}
|
||||
self.simple_http_resources = {}
|
||||
self.mgr = ServerManager(self.certs, self.simple_http_resources)
|
||||
self.http_01_resources = {}
|
||||
self.mgr = ServerManager(self.certs, self.http_01_resources)
|
||||
|
||||
def test_init(self):
|
||||
self.assertTrue(self.mgr.certs is self.certs)
|
||||
self.assertTrue(
|
||||
self.mgr.simple_http_resources is self.simple_http_resources)
|
||||
self.mgr.http_01_resources is self.http_01_resources)
|
||||
|
||||
def _test_run_stop(self, challenge_type):
|
||||
server = self.mgr.run(port=0, challenge_type=challenge_type)
|
||||
@@ -39,10 +39,10 @@ class ServerManagerTest(unittest.TestCase):
|
||||
self.mgr.stop(port=port)
|
||||
self.assertEqual(self.mgr.running(), {})
|
||||
|
||||
def test_run_stop_dvsni(self):
|
||||
self._test_run_stop(challenges.DVSNI)
|
||||
def test_run_stop_tls_sni_01(self):
|
||||
self._test_run_stop(challenges.TLSSNI01)
|
||||
|
||||
def test_run_stop_simplehttp(self):
|
||||
def test_run_stop_http_01(self):
|
||||
self._test_run_stop(challenges.HTTP01)
|
||||
|
||||
def test_run_idempotent(self):
|
||||
@@ -73,10 +73,10 @@ class SupportedChallengesValidatorTest(unittest.TestCase):
|
||||
return supported_challenges_validator(data)
|
||||
|
||||
def test_correct(self):
|
||||
self.assertEqual("dvsni", self._call("dvsni"))
|
||||
self.assertEqual("tls-sni-01", self._call("tls-sni-01"))
|
||||
self.assertEqual("http-01", self._call("http-01"))
|
||||
self.assertEqual("dvsni,http-01", self._call("dvsni,http-01"))
|
||||
self.assertEqual("http-01,dvsni", self._call("http-01,dvsni"))
|
||||
self.assertEqual("tls-sni-01,http-01", self._call("tls-sni-01,http-01"))
|
||||
self.assertEqual("http-01,tls-sni-01", self._call("http-01,tls-sni-01"))
|
||||
|
||||
def test_unrecognized(self):
|
||||
assert "foo" not in challenges.Challenge.TYPES
|
||||
@@ -92,24 +92,24 @@ class AuthenticatorTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
from letsencrypt.plugins.standalone import Authenticator
|
||||
self.config = mock.MagicMock(
|
||||
dvsni_port=1234, http01_port=4321,
|
||||
standalone_supported_challenges="dvsni,http-01")
|
||||
tls_sni_01_port=1234, http01_port=4321,
|
||||
standalone_supported_challenges="tls-sni-01,http-01")
|
||||
self.auth = Authenticator(self.config, name="standalone")
|
||||
|
||||
def test_supported_challenges(self):
|
||||
self.assertEqual(self.auth.supported_challenges,
|
||||
set([challenges.DVSNI, challenges.HTTP01]))
|
||||
set([challenges.TLSSNI01, challenges.HTTP01]))
|
||||
|
||||
def test_more_info(self):
|
||||
self.assertTrue(isinstance(self.auth.more_info(), six.string_types))
|
||||
|
||||
def test_get_chall_pref(self):
|
||||
self.assertEqual(set(self.auth.get_chall_pref(domain=None)),
|
||||
set([challenges.DVSNI, challenges.HTTP01]))
|
||||
set([challenges.TLSSNI01, challenges.HTTP01]))
|
||||
|
||||
@mock.patch("letsencrypt.plugins.standalone.util")
|
||||
def test_perform_alredy_listening(self, mock_util):
|
||||
for chall, port in ((challenges.DVSNI.typ, 1234),
|
||||
for chall, port in ((challenges.TLSSNI01.typ, 1234),
|
||||
(challenges.HTTP01.typ, 4321)):
|
||||
mock_util.already_listening.return_value = True
|
||||
self.config.standalone_supported_challenges = chall
|
||||
@@ -153,10 +153,10 @@ class AuthenticatorTest(unittest.TestCase):
|
||||
def test_perform2(self):
|
||||
domain = b'localhost'
|
||||
key = jose.JWK.load(test_util.load_vector('rsa512_key.pem'))
|
||||
simple_http = achallenges.KeyAuthorizationAnnotatedChallenge(
|
||||
http_01 = achallenges.KeyAuthorizationAnnotatedChallenge(
|
||||
challb=acme_util.HTTP01_P, domain=domain, account_key=key)
|
||||
dvsni = achallenges.DVSNI(
|
||||
challb=acme_util.DVSNI_P, domain=domain, account_key=key)
|
||||
tls_sni_01 = achallenges.KeyAuthorizationAnnotatedChallenge(
|
||||
challb=acme_util.TLSSNI01_P, domain=domain, account_key=key)
|
||||
|
||||
self.auth.servers = mock.MagicMock()
|
||||
|
||||
@@ -164,24 +164,24 @@ class AuthenticatorTest(unittest.TestCase):
|
||||
return "server{0}".format(port)
|
||||
|
||||
self.auth.servers.run.side_effect = _run
|
||||
responses = self.auth.perform2([simple_http, dvsni])
|
||||
responses = self.auth.perform2([http_01, tls_sni_01])
|
||||
|
||||
self.assertTrue(isinstance(responses, list))
|
||||
self.assertEqual(2, len(responses))
|
||||
self.assertTrue(isinstance(responses[0], challenges.HTTP01Response))
|
||||
self.assertTrue(isinstance(responses[1], challenges.DVSNIResponse))
|
||||
self.assertTrue(isinstance(responses[1], challenges.TLSSNI01Response))
|
||||
|
||||
self.assertEqual(self.auth.servers.run.mock_calls, [
|
||||
mock.call(4321, challenges.HTTP01),
|
||||
mock.call(1234, challenges.DVSNI),
|
||||
mock.call(1234, challenges.TLSSNI01),
|
||||
])
|
||||
self.assertEqual(self.auth.served, {
|
||||
"server1234": set([dvsni]),
|
||||
"server4321": set([simple_http]),
|
||||
"server1234": set([tls_sni_01]),
|
||||
"server4321": set([http_01]),
|
||||
})
|
||||
self.assertEqual(1, len(self.auth.simple_http_resources))
|
||||
self.assertEqual(2, len(self.auth.certs))
|
||||
self.assertEqual(list(self.auth.simple_http_resources), [
|
||||
self.assertEqual(1, len(self.auth.http_01_resources))
|
||||
self.assertEqual(1, len(self.auth.certs))
|
||||
self.assertEqual(list(self.auth.http_01_resources), [
|
||||
acme_standalone.HTTP01RequestHandler.HTTP01Resource(
|
||||
acme_util.HTTP01, responses[0], mock.ANY)])
|
||||
|
||||
|
||||
@@ -75,7 +75,7 @@ def renew(cert, old_version):
|
||||
# XXX: this loses type data (for example, the fact that key_size
|
||||
# was an int, not a str)
|
||||
config.rsa_key_size = int(config.rsa_key_size)
|
||||
config.dvsni_port = int(config.dvsni_port)
|
||||
config.tls_sni_01_port = int(config.tls_sni_01_port)
|
||||
config.namespace.http01_port = int(config.namespace.http01_port)
|
||||
zope.component.provideUtility(config)
|
||||
try:
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
"""Tests for letsencrypt.achallenges."""
|
||||
import unittest
|
||||
|
||||
import OpenSSL
|
||||
|
||||
from acme import challenges
|
||||
from acme import jose
|
||||
|
||||
from letsencrypt.tests import acme_util
|
||||
from letsencrypt.tests import test_util
|
||||
|
||||
|
||||
class DVSNITest(unittest.TestCase):
|
||||
"""Tests for letsencrypt.achallenges.DVSNI."""
|
||||
|
||||
def setUp(self):
|
||||
self.challb = acme_util.chall_to_challb(acme_util.DVSNI, "pending")
|
||||
key = jose.JWKRSA.load(test_util.load_vector("rsa512_key.pem"))
|
||||
from letsencrypt.achallenges import DVSNI
|
||||
self.achall = DVSNI(
|
||||
challb=self.challb, domain="example.com", account_key=key)
|
||||
|
||||
def test_proxy(self):
|
||||
self.assertEqual(self.challb.token, self.achall.token)
|
||||
|
||||
def test_gen_cert_and_response(self):
|
||||
response, cert, key = self.achall.gen_cert_and_response()
|
||||
self.assertTrue(isinstance(response, challenges.DVSNIResponse))
|
||||
self.assertTrue(isinstance(cert, OpenSSL.crypto.X509))
|
||||
self.assertTrue(isinstance(key, OpenSSL.crypto.PKey))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main() # pragma: no cover
|
||||
@@ -14,7 +14,7 @@ KEY = test_util.load_rsa_private_key('rsa512_key.pem')
|
||||
# Challenges
|
||||
HTTP01 = challenges.HTTP01(
|
||||
token="evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ+PCt92wr+oA")
|
||||
DVSNI = challenges.DVSNI(
|
||||
TLSSNI01 = challenges.TLSSNI01(
|
||||
token=jose.b64decode(b"evaGxfADs6pSRb2LAv9IZf17Dt3juxGJyPCt92wrDoA"))
|
||||
DNS = challenges.DNS(token="17817c66b60ce2e4012dfad92657527a")
|
||||
RECOVERY_CONTACT = challenges.RecoveryContact(
|
||||
@@ -41,7 +41,7 @@ POP = challenges.ProofOfPossession(
|
||||
)
|
||||
)
|
||||
|
||||
CHALLENGES = [HTTP01, DVSNI, DNS, RECOVERY_CONTACT, POP]
|
||||
CHALLENGES = [HTTP01, TLSSNI01, DNS, RECOVERY_CONTACT, POP]
|
||||
DV_CHALLENGES = [chall for chall in CHALLENGES
|
||||
if isinstance(chall, challenges.DVChallenge)]
|
||||
CONT_CHALLENGES = [chall for chall in CHALLENGES
|
||||
@@ -79,13 +79,13 @@ def chall_to_challb(chall, status): # pylint: disable=redefined-outer-name
|
||||
|
||||
|
||||
# Pending ChallengeBody objects
|
||||
DVSNI_P = chall_to_challb(DVSNI, messages.STATUS_PENDING)
|
||||
TLSSNI01_P = chall_to_challb(TLSSNI01, messages.STATUS_PENDING)
|
||||
HTTP01_P = chall_to_challb(HTTP01, messages.STATUS_PENDING)
|
||||
DNS_P = chall_to_challb(DNS, messages.STATUS_PENDING)
|
||||
RECOVERY_CONTACT_P = chall_to_challb(RECOVERY_CONTACT, messages.STATUS_PENDING)
|
||||
POP_P = chall_to_challb(POP, messages.STATUS_PENDING)
|
||||
|
||||
CHALLENGES_P = [HTTP01_P, DVSNI_P, DNS_P, RECOVERY_CONTACT_P, POP_P]
|
||||
CHALLENGES_P = [HTTP01_P, TLSSNI01_P, DNS_P, RECOVERY_CONTACT_P, POP_P]
|
||||
DV_CHALLENGES_P = [challb for challb in CHALLENGES_P
|
||||
if isinstance(challb.chall, challenges.DVChallenge)]
|
||||
CONT_CHALLENGES_P = [
|
||||
|
||||
@@ -45,7 +45,7 @@ class ChallengeFactoryTest(unittest.TestCase):
|
||||
|
||||
self.assertEqual(
|
||||
[achall.chall for achall in cont_c], [acme_util.RECOVERY_CONTACT])
|
||||
self.assertEqual([achall.chall for achall in dv_c], [acme_util.DVSNI])
|
||||
self.assertEqual([achall.chall for achall in dv_c], [acme_util.TLSSNI01])
|
||||
|
||||
def test_unrecognized(self):
|
||||
self.handler.authzr["failure.com"] = acme_util.gen_authzr(
|
||||
@@ -70,7 +70,7 @@ class GetAuthorizationsTest(unittest.TestCase):
|
||||
self.mock_dv_auth = mock.MagicMock(name="ApacheConfigurator")
|
||||
self.mock_cont_auth = mock.MagicMock(name="ContinuityAuthenticator")
|
||||
|
||||
self.mock_dv_auth.get_chall_pref.return_value = [challenges.DVSNI]
|
||||
self.mock_dv_auth.get_chall_pref.return_value = [challenges.TLSSNI01]
|
||||
self.mock_cont_auth.get_chall_pref.return_value = [
|
||||
challenges.RecoveryContact]
|
||||
|
||||
@@ -90,7 +90,7 @@ class GetAuthorizationsTest(unittest.TestCase):
|
||||
logging.disable(logging.NOTSET)
|
||||
|
||||
@mock.patch("letsencrypt.auth_handler.AuthHandler._poll_challenges")
|
||||
def test_name1_dvsni1(self, mock_poll):
|
||||
def test_name1_tls_sni_01_1(self, mock_poll):
|
||||
self.mock_net.request_domain_challenges.side_effect = functools.partial(
|
||||
gen_dom_authzr, challs=acme_util.DV_CHALLENGES)
|
||||
|
||||
@@ -107,14 +107,14 @@ class GetAuthorizationsTest(unittest.TestCase):
|
||||
|
||||
self.assertEqual(self.mock_dv_auth.cleanup.call_count, 1)
|
||||
self.assertEqual(self.mock_cont_auth.cleanup.call_count, 0)
|
||||
# Test if list first element is DVSNI, use typ because it is an achall
|
||||
# Test if list first element is TLSSNI01, use typ because it is an achall
|
||||
self.assertEqual(
|
||||
self.mock_dv_auth.cleanup.call_args[0][0][0].typ, "dvsni")
|
||||
self.mock_dv_auth.cleanup.call_args[0][0][0].typ, "tls-sni-01")
|
||||
|
||||
self.assertEqual(len(authzr), 1)
|
||||
|
||||
@mock.patch("letsencrypt.auth_handler.AuthHandler._poll_challenges")
|
||||
def test_name3_dvsni3_rectok_3(self, mock_poll):
|
||||
def test_name3_tls_sni_01_3_rectok_3(self, mock_poll):
|
||||
self.mock_net.request_domain_challenges.side_effect = functools.partial(
|
||||
gen_dom_authzr, challs=acme_util.CHALLENGES)
|
||||
|
||||
@@ -309,9 +309,9 @@ class GenChallengePathTest(unittest.TestCase):
|
||||
return gen_challenge_path(challbs, preferences, combinations)
|
||||
|
||||
def test_common_case(self):
|
||||
"""Given DVSNI and HTTP01 with appropriate combos."""
|
||||
challbs = (acme_util.DVSNI_P, acme_util.HTTP01_P)
|
||||
prefs = [challenges.DVSNI]
|
||||
"""Given TLSSNI01 and HTTP01 with appropriate combos."""
|
||||
challbs = (acme_util.TLSSNI01_P, acme_util.HTTP01_P)
|
||||
prefs = [challenges.TLSSNI01]
|
||||
combos = ((0,), (1,))
|
||||
|
||||
# Smart then trivial dumb path test
|
||||
@@ -324,9 +324,9 @@ class GenChallengePathTest(unittest.TestCase):
|
||||
def test_common_case_with_continuity(self):
|
||||
challbs = (acme_util.POP_P,
|
||||
acme_util.RECOVERY_CONTACT_P,
|
||||
acme_util.DVSNI_P,
|
||||
acme_util.TLSSNI01_P,
|
||||
acme_util.HTTP01_P)
|
||||
prefs = [challenges.ProofOfPossession, challenges.DVSNI]
|
||||
prefs = [challenges.ProofOfPossession, challenges.TLSSNI01]
|
||||
combos = acme_util.gen_combos(challbs)
|
||||
self.assertEqual(self._call(challbs, prefs, combos), (0, 2))
|
||||
|
||||
@@ -336,14 +336,14 @@ class GenChallengePathTest(unittest.TestCase):
|
||||
def test_full_cont_server(self):
|
||||
challbs = (acme_util.RECOVERY_CONTACT_P,
|
||||
acme_util.POP_P,
|
||||
acme_util.DVSNI_P,
|
||||
acme_util.TLSSNI01_P,
|
||||
acme_util.HTTP01_P,
|
||||
acme_util.DNS_P)
|
||||
# Typical webserver client that can do everything except DNS
|
||||
# Attempted to make the order realistic
|
||||
prefs = [challenges.ProofOfPossession,
|
||||
challenges.HTTP01,
|
||||
challenges.DVSNI,
|
||||
challenges.TLSSNI01,
|
||||
challenges.RecoveryContact]
|
||||
combos = acme_util.gen_combos(challbs)
|
||||
self.assertEqual(self._call(challbs, prefs, combos), (1, 3))
|
||||
@@ -352,8 +352,8 @@ class GenChallengePathTest(unittest.TestCase):
|
||||
self.assertTrue(self._call(challbs, prefs, None))
|
||||
|
||||
def test_not_supported(self):
|
||||
challbs = (acme_util.POP_P, acme_util.DVSNI_P)
|
||||
prefs = [challenges.DVSNI]
|
||||
challbs = (acme_util.POP_P, acme_util.TLSSNI01_P)
|
||||
prefs = [challenges.TLSSNI01]
|
||||
combos = ((0, 1),)
|
||||
|
||||
self.assertRaises(
|
||||
@@ -411,7 +411,7 @@ class IsPreferredTest(unittest.TestCase):
|
||||
def _call(cls, chall, satisfied):
|
||||
from letsencrypt.auth_handler import is_preferred
|
||||
return is_preferred(chall, satisfied, exclusive_groups=frozenset([
|
||||
frozenset([challenges.DVSNI, challenges.HTTP01]),
|
||||
frozenset([challenges.TLSSNI01, challenges.HTTP01]),
|
||||
frozenset([challenges.DNS, challenges.HTTP01]),
|
||||
]))
|
||||
|
||||
@@ -421,11 +421,11 @@ class IsPreferredTest(unittest.TestCase):
|
||||
def test_mutually_exclusvie(self):
|
||||
self.assertFalse(
|
||||
self._call(
|
||||
acme_util.DVSNI_P, frozenset([acme_util.HTTP01_P])))
|
||||
acme_util.TLSSNI01_P, frozenset([acme_util.HTTP01_P])))
|
||||
|
||||
def test_mutually_exclusive_same_type(self):
|
||||
self.assertTrue(
|
||||
self._call(acme_util.DVSNI_P, frozenset([acme_util.DVSNI_P])))
|
||||
self._call(acme_util.TLSSNI01_P, frozenset([acme_util.TLSSNI01_P])))
|
||||
|
||||
|
||||
class ReportFailedChallsTest(unittest.TestCase):
|
||||
@@ -446,15 +446,15 @@ class ReportFailedChallsTest(unittest.TestCase):
|
||||
domain="example.com",
|
||||
account_key="key")
|
||||
|
||||
kwargs["chall"] = acme_util.DVSNI
|
||||
self.dvsni_same = achallenges.DVSNI(
|
||||
kwargs["chall"] = acme_util.TLSSNI01
|
||||
self.tls_sni_same = achallenges.KeyAuthorizationAnnotatedChallenge(
|
||||
# pylint: disable=star-args
|
||||
challb=messages.ChallengeBody(**kwargs),
|
||||
domain="example.com",
|
||||
account_key="key")
|
||||
|
||||
kwargs["error"] = messages.Error(typ="dnssec", detail="detail")
|
||||
self.dvsni_diff = achallenges.DVSNI(
|
||||
self.tls_sni_diff = achallenges.KeyAuthorizationAnnotatedChallenge(
|
||||
# pylint: disable=star-args
|
||||
challb=messages.ChallengeBody(**kwargs),
|
||||
domain="foo.bar",
|
||||
@@ -464,7 +464,7 @@ class ReportFailedChallsTest(unittest.TestCase):
|
||||
def test_same_error_and_domain(self, mock_zope):
|
||||
from letsencrypt import auth_handler
|
||||
|
||||
auth_handler._report_failed_challs([self.http01, self.dvsni_same])
|
||||
auth_handler._report_failed_challs([self.http01, self.tls_sni_same])
|
||||
call_list = mock_zope().add_message.call_args_list
|
||||
self.assertTrue(len(call_list) == 1)
|
||||
self.assertTrue("Domains: example.com\n" in call_list[0][0][0])
|
||||
@@ -473,7 +473,7 @@ class ReportFailedChallsTest(unittest.TestCase):
|
||||
def test_different_errors_and_domains(self, mock_zope):
|
||||
from letsencrypt import auth_handler
|
||||
|
||||
auth_handler._report_failed_challs([self.http01, self.dvsni_diff])
|
||||
auth_handler._report_failed_challs([self.http01, self.tls_sni_diff])
|
||||
self.assertTrue(mock_zope().add_message.call_count == 2)
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"""Tests for letsencrypt.cli."""
|
||||
import argparse
|
||||
import itertools
|
||||
import os
|
||||
import shutil
|
||||
@@ -9,9 +10,14 @@ import unittest
|
||||
|
||||
import mock
|
||||
|
||||
from acme import jose
|
||||
|
||||
from letsencrypt import account
|
||||
from letsencrypt import cli
|
||||
from letsencrypt import configuration
|
||||
from letsencrypt import crypto_util
|
||||
from letsencrypt import errors
|
||||
from letsencrypt import le_util
|
||||
|
||||
from letsencrypt.plugins import disco
|
||||
|
||||
@@ -19,10 +25,12 @@ from letsencrypt.tests import renewer_test
|
||||
from letsencrypt.tests import test_util
|
||||
|
||||
|
||||
CERT = test_util.vector_path('cert.pem')
|
||||
CSR = test_util.vector_path('csr.der')
|
||||
KEY = test_util.vector_path('rsa256_key.pem')
|
||||
|
||||
|
||||
class CLITest(unittest.TestCase):
|
||||
class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods
|
||||
"""Tests for different commands."""
|
||||
|
||||
def setUp(self):
|
||||
@@ -30,33 +38,36 @@ class CLITest(unittest.TestCase):
|
||||
self.config_dir = os.path.join(self.tmp_dir, 'config')
|
||||
self.work_dir = os.path.join(self.tmp_dir, 'work')
|
||||
self.logs_dir = os.path.join(self.tmp_dir, 'logs')
|
||||
self.standard_args = ['--text', '--config-dir', self.config_dir,
|
||||
'--work-dir', self.work_dir, '--logs-dir', self.logs_dir,
|
||||
'--agree-dev-preview']
|
||||
|
||||
def tearDown(self):
|
||||
shutil.rmtree(self.tmp_dir)
|
||||
|
||||
def _call(self, args):
|
||||
from letsencrypt import cli
|
||||
args = ['--text', '--config-dir', self.config_dir,
|
||||
'--work-dir', self.work_dir, '--logs-dir', self.logs_dir,
|
||||
'--agree-dev-preview'] + args
|
||||
"Run the cli with output streams and actual client mocked out"
|
||||
with mock.patch('letsencrypt.cli.client') as client:
|
||||
ret, stdout, stderr = self._call_no_clientmock(args)
|
||||
return ret, stdout, stderr, client
|
||||
|
||||
def _call_no_clientmock(self, args):
|
||||
"Run the client with output streams mocked out"
|
||||
args = self.standard_args + args
|
||||
with mock.patch('letsencrypt.cli.sys.stdout') as stdout:
|
||||
with mock.patch('letsencrypt.cli.sys.stderr') as stderr:
|
||||
with mock.patch('letsencrypt.cli.client') as client:
|
||||
ret = cli.main(args)
|
||||
return ret, stdout, stderr, client
|
||||
ret = cli.main(args[:]) # NOTE: parser can alter its args!
|
||||
return ret, stdout, stderr
|
||||
|
||||
def _call_stdout(self, args):
|
||||
"""
|
||||
Variant of _call that preserves stdout so that it can be mocked by the
|
||||
caller.
|
||||
"""
|
||||
from letsencrypt import cli
|
||||
args = ['--text', '--config-dir', self.config_dir,
|
||||
'--work-dir', self.work_dir, '--logs-dir', self.logs_dir,
|
||||
'--agree-dev-preview'] + args
|
||||
args = self.standard_args + args
|
||||
with mock.patch('letsencrypt.cli.sys.stderr') as stderr:
|
||||
with mock.patch('letsencrypt.cli.client') as client:
|
||||
ret = cli.main(args)
|
||||
ret = cli.main(args[:]) # NOTE: parser can alter its args!
|
||||
return ret, None, stderr, client
|
||||
|
||||
def test_no_flags(self):
|
||||
@@ -112,16 +123,61 @@ class CLITest(unittest.TestCase):
|
||||
self.assertTrue("--key-path" not in out)
|
||||
|
||||
out = self._help_output(['-h'])
|
||||
from letsencrypt import cli
|
||||
self.assertTrue(cli.usage_strings(plugins)[0] in out)
|
||||
|
||||
@mock.patch('letsencrypt.cli.client.acme_client.Client')
|
||||
@mock.patch('letsencrypt.cli._determine_account')
|
||||
@mock.patch('letsencrypt.cli.client.Client.obtain_and_enroll_certificate')
|
||||
@mock.patch('letsencrypt.cli._auth_from_domains')
|
||||
def test_user_agent(self, _afd, _obt, det, _client):
|
||||
# Normally the client is totally mocked out, but here we need more
|
||||
# arguments to automate it...
|
||||
args = ["--standalone", "certonly", "-m", "none@none.com",
|
||||
"-d", "example.com", '--agree-tos'] + self.standard_args
|
||||
det.return_value = mock.MagicMock(), None
|
||||
with mock.patch('letsencrypt.cli.client.acme_client.ClientNetwork') as acme_net:
|
||||
self._call_no_clientmock(args)
|
||||
os_ver = " ".join(le_util.get_os_info())
|
||||
ua = acme_net.call_args[1]["user_agent"]
|
||||
self.assertTrue(os_ver in ua)
|
||||
import platform
|
||||
plat = platform.platform()
|
||||
if "linux" in plat.lower():
|
||||
self.assertTrue(platform.linux_distribution()[0] in ua)
|
||||
|
||||
with mock.patch('letsencrypt.cli.client.acme_client.ClientNetwork') as acme_net:
|
||||
ua = "bandersnatch"
|
||||
args += ["--user-agent", ua]
|
||||
self._call_no_clientmock(args)
|
||||
acme_net.assert_called_once_with(mock.ANY, verify_ssl=True, user_agent=ua)
|
||||
|
||||
def test_install_abspath(self):
|
||||
cert = 'cert'
|
||||
key = 'key'
|
||||
chain = 'chain'
|
||||
fullchain = 'fullchain'
|
||||
|
||||
with MockedVerb('install') as mock_install:
|
||||
self._call(['install', '--cert-path', cert, '--key-path', 'key',
|
||||
'--chain-path', 'chain',
|
||||
'--fullchain-path', 'fullchain'])
|
||||
|
||||
args = mock_install.call_args[0][0]
|
||||
self.assertEqual(args.cert_path, os.path.abspath(cert))
|
||||
self.assertEqual(args.key_path, os.path.abspath(key))
|
||||
self.assertEqual(args.chain_path, os.path.abspath(chain))
|
||||
self.assertEqual(args.fullchain_path, os.path.abspath(fullchain))
|
||||
|
||||
@mock.patch('letsencrypt.cli.record_chosen_plugins')
|
||||
@mock.patch('letsencrypt.cli.display_ops')
|
||||
def test_installer_selection(self, mock_display_ops):
|
||||
self._call(['install', '--domain', 'foo.bar', '--cert-path', 'cert',
|
||||
def test_installer_selection(self, mock_display_ops, _rec):
|
||||
self._call(['install', '--domains', 'foo.bar', '--cert-path', 'cert',
|
||||
'--key-path', 'key', '--chain-path', 'chain'])
|
||||
self.assertEqual(mock_display_ops.pick_installer.call_count, 1)
|
||||
|
||||
def test_configurator_selection(self):
|
||||
@mock.patch('letsencrypt.le_util.exe_exists')
|
||||
def test_configurator_selection(self, mock_exe_exists):
|
||||
mock_exe_exists.return_value = True
|
||||
real_plugins = disco.PluginsRegistry.find_all()
|
||||
args = ['--agree-dev-preview', '--apache',
|
||||
'--authenticator', 'standalone']
|
||||
@@ -168,6 +224,65 @@ class CLITest(unittest.TestCase):
|
||||
for r in xrange(len(flags)))):
|
||||
self._call(['plugins'] + list(args))
|
||||
|
||||
@mock.patch('letsencrypt.cli.plugins_disco')
|
||||
def test_plugins_no_args(self, mock_disco):
|
||||
ifaces = []
|
||||
plugins = mock_disco.PluginsRegistry.find_all()
|
||||
|
||||
_, stdout, _, _ = self._call(['plugins'])
|
||||
plugins.visible.assert_called_once_with()
|
||||
plugins.visible().ifaces.assert_called_once_with(ifaces)
|
||||
filtered = plugins.visible().ifaces()
|
||||
stdout.write.called_once_with(str(filtered))
|
||||
|
||||
@mock.patch('letsencrypt.cli.plugins_disco')
|
||||
def test_plugins_init(self, mock_disco):
|
||||
ifaces = []
|
||||
plugins = mock_disco.PluginsRegistry.find_all()
|
||||
|
||||
_, stdout, _, _ = self._call(['plugins', '--init'])
|
||||
plugins.visible.assert_called_once_with()
|
||||
plugins.visible().ifaces.assert_called_once_with(ifaces)
|
||||
filtered = plugins.visible().ifaces()
|
||||
self.assertEqual(filtered.init.call_count, 1)
|
||||
filtered.verify.assert_called_once_with(ifaces)
|
||||
verified = filtered.verify()
|
||||
stdout.write.called_once_with(str(verified))
|
||||
|
||||
@mock.patch('letsencrypt.cli.plugins_disco')
|
||||
def test_plugins_prepare(self, mock_disco):
|
||||
ifaces = []
|
||||
plugins = mock_disco.PluginsRegistry.find_all()
|
||||
|
||||
_, stdout, _, _ = self._call(['plugins', '--init', '--prepare'])
|
||||
plugins.visible.assert_called_once_with()
|
||||
plugins.visible().ifaces.assert_called_once_with(ifaces)
|
||||
filtered = plugins.visible().ifaces()
|
||||
self.assertEqual(filtered.init.call_count, 1)
|
||||
filtered.verify.assert_called_once_with(ifaces)
|
||||
verified = filtered.verify()
|
||||
verified.prepare.assert_called_once_with()
|
||||
verified.available.assert_called_once_with()
|
||||
available = verified.available()
|
||||
stdout.write.called_once_with(str(available))
|
||||
|
||||
def test_certonly_abspath(self):
|
||||
cert = 'cert'
|
||||
key = 'key'
|
||||
chain = 'chain'
|
||||
fullchain = 'fullchain'
|
||||
|
||||
with MockedVerb('certonly') as mock_obtaincert:
|
||||
self._call(['certonly', '--cert-path', cert, '--key-path', 'key',
|
||||
'--chain-path', 'chain',
|
||||
'--fullchain-path', 'fullchain'])
|
||||
|
||||
args = mock_obtaincert.call_args[0][0]
|
||||
self.assertEqual(args.cert_path, os.path.abspath(cert))
|
||||
self.assertEqual(args.key_path, os.path.abspath(key))
|
||||
self.assertEqual(args.chain_path, os.path.abspath(chain))
|
||||
self.assertEqual(args.fullchain_path, os.path.abspath(fullchain))
|
||||
|
||||
def test_certonly_bad_args(self):
|
||||
ret, _, _, _ = self._call(['-d', 'foo.bar', 'certonly', '--csr', CSR])
|
||||
self.assertEqual(ret, '--domains and --csr are mutually exclusive')
|
||||
@@ -175,6 +290,44 @@ class CLITest(unittest.TestCase):
|
||||
ret, _, _, _ = self._call(['-a', 'bad_auth', 'certonly'])
|
||||
self.assertEqual(ret, 'The requested bad_auth plugin does not appear to be installed')
|
||||
|
||||
def test_check_config_sanity_domain(self):
|
||||
# Punycode
|
||||
self.assertRaises(errors.ConfigurationError,
|
||||
self._call,
|
||||
['-d', 'this.is.xn--ls8h.tld'])
|
||||
# FQDN
|
||||
self.assertRaises(errors.ConfigurationError,
|
||||
self._call,
|
||||
['-d', 'comma,gotwrong.tld'])
|
||||
# FQDN 2
|
||||
self.assertRaises(errors.ConfigurationError,
|
||||
self._call,
|
||||
['-d', 'illegal.character=.tld'])
|
||||
# Wildcard
|
||||
self.assertRaises(errors.ConfigurationError,
|
||||
self._call,
|
||||
['-d', '*.wildcard.tld'])
|
||||
|
||||
def test_parse_domains(self):
|
||||
plugins = disco.PluginsRegistry.find_all()
|
||||
|
||||
short_args = ['-d', 'example.com']
|
||||
namespace = cli.prepare_and_parse_args(plugins, short_args)
|
||||
self.assertEqual(namespace.domains, ['example.com'])
|
||||
|
||||
short_args = ['-d', 'example.com,another.net,third.org,example.com']
|
||||
namespace = cli.prepare_and_parse_args(plugins, short_args)
|
||||
self.assertEqual(namespace.domains, ['example.com', 'another.net',
|
||||
'third.org'])
|
||||
|
||||
long_args = ['--domains', 'example.com']
|
||||
namespace = cli.prepare_and_parse_args(plugins, long_args)
|
||||
self.assertEqual(namespace.domains, ['example.com'])
|
||||
|
||||
long_args = ['--domains', 'example.com,another.net,example.com']
|
||||
namespace = cli.prepare_and_parse_args(plugins, long_args)
|
||||
self.assertEqual(namespace.domains, ['example.com', 'another.net'])
|
||||
|
||||
@mock.patch('letsencrypt.crypto_util.notAfter')
|
||||
@mock.patch('letsencrypt.cli.zope.component.getUtility')
|
||||
def test_certonly_new_request_success(self, mock_get_utility, mock_notAfter):
|
||||
@@ -235,7 +388,8 @@ class CLITest(unittest.TestCase):
|
||||
@mock.patch('letsencrypt.cli.display_ops.pick_installer')
|
||||
@mock.patch('letsencrypt.cli.zope.component.getUtility')
|
||||
@mock.patch('letsencrypt.cli._init_le_client')
|
||||
def test_certonly_csr(self, mock_init, mock_get_utility,
|
||||
@mock.patch('letsencrypt.cli.record_chosen_plugins')
|
||||
def test_certonly_csr(self, _rec, mock_init, mock_get_utility,
|
||||
mock_pick_installer, mock_notAfter):
|
||||
cert_path = '/etc/letsencrypt/live/blahcert.pem'
|
||||
date = '1970-01-01'
|
||||
@@ -260,11 +414,31 @@ class CLITest(unittest.TestCase):
|
||||
self.assertTrue(
|
||||
date in mock_get_utility().add_message.call_args[0][0])
|
||||
|
||||
@mock.patch('letsencrypt.cli.client.acme_client')
|
||||
def test_revoke_with_key(self, mock_acme_client):
|
||||
server = 'foo.bar'
|
||||
self._call_no_clientmock(['--cert-path', CERT, '--key-path', KEY,
|
||||
'--server', server, 'revoke'])
|
||||
with open(KEY) as f:
|
||||
mock_acme_client.Client.assert_called_once_with(
|
||||
server, key=jose.JWK.load(f.read()), net=mock.ANY)
|
||||
with open(CERT) as f:
|
||||
cert = crypto_util.pyopenssl_load_certificate(f.read())[0]
|
||||
mock_revoke = mock_acme_client.Client().revoke
|
||||
mock_revoke.assert_called_once_with(jose.ComparableX509(cert))
|
||||
|
||||
@mock.patch('letsencrypt.cli._determine_account')
|
||||
def test_revoke_without_key(self, mock_determine_account):
|
||||
mock_determine_account.return_value = (mock.MagicMock(), None)
|
||||
_, _, _, client = self._call(['--cert-path', CERT, 'revoke'])
|
||||
with open(CERT) as f:
|
||||
cert = crypto_util.pyopenssl_load_certificate(f.read())[0]
|
||||
mock_revoke = client.acme_from_config_key().revoke
|
||||
mock_revoke.assert_called_once_with(jose.ComparableX509(cert))
|
||||
|
||||
@mock.patch('letsencrypt.cli.sys')
|
||||
def test_handle_exception(self, mock_sys):
|
||||
# pylint: disable=protected-access
|
||||
from letsencrypt import cli
|
||||
|
||||
mock_open = mock.mock_open()
|
||||
with mock.patch('letsencrypt.cli.open', mock_open, create=True):
|
||||
exception = Exception('detail')
|
||||
@@ -296,6 +470,19 @@ class CLITest(unittest.TestCase):
|
||||
mock_sys.exit.assert_called_with(''.join(
|
||||
traceback.format_exception_only(KeyboardInterrupt, interrupt)))
|
||||
|
||||
def test_read_file(self):
|
||||
rel_test_path = os.path.relpath(os.path.join(self.tmp_dir, 'foo'))
|
||||
self.assertRaises(
|
||||
argparse.ArgumentTypeError, cli.read_file, rel_test_path)
|
||||
|
||||
test_contents = 'bar\n'
|
||||
with open(rel_test_path, 'w') as f:
|
||||
f.write(test_contents)
|
||||
|
||||
path, contents = cli.read_file(rel_test_path)
|
||||
self.assertEqual(path, os.path.abspath(path))
|
||||
self.assertEqual(contents, test_contents)
|
||||
|
||||
|
||||
class DetermineAccountTest(unittest.TestCase):
|
||||
"""Tests for letsencrypt.cli._determine_account."""
|
||||
@@ -415,8 +602,6 @@ class MockedVerb(object):
|
||||
|
||||
"""
|
||||
def __init__(self, verb_name):
|
||||
from letsencrypt import cli
|
||||
|
||||
self.verb_dict = cli.HelpfulArgumentParser.VERBS
|
||||
self.verb_func = None
|
||||
self.verb_name = verb_name
|
||||
|
||||
@@ -70,8 +70,8 @@ class ClientTest(unittest.TestCase):
|
||||
dv_auth=None, installer=None)
|
||||
|
||||
def test_init_acme_verify_ssl(self):
|
||||
self.acme_client.assert_called_once_with(
|
||||
directory=mock.ANY, key=mock.ANY, verify_ssl=True)
|
||||
net = self.acme_client.call_args[1]["net"]
|
||||
self.assertTrue(net.verify_ssl)
|
||||
|
||||
def _mock_obtain_certificate(self):
|
||||
self.client.auth_handler = mock.MagicMock()
|
||||
@@ -148,7 +148,7 @@ class ClientTest(unittest.TestCase):
|
||||
|
||||
shutil.rmtree(tmp_path)
|
||||
|
||||
def test_deploy_certificate(self):
|
||||
def test_deploy_certificate_success(self):
|
||||
self.assertRaises(errors.Error, self.client.deploy_certificate,
|
||||
["foo.bar"], "key", "cert", "chain", "fullchain")
|
||||
|
||||
@@ -166,17 +166,38 @@ class ClientTest(unittest.TestCase):
|
||||
self.assertEqual(installer.save.call_count, 2)
|
||||
installer.restart.assert_called_once_with()
|
||||
|
||||
def test_deploy_certificate_restart_failure_with_recovery(self):
|
||||
def test_deploy_certificate_failure(self):
|
||||
installer = mock.MagicMock()
|
||||
self.client.installer = installer
|
||||
|
||||
installer.deploy_cert.side_effect = errors.PluginError
|
||||
self.assertRaises(errors.PluginError, self.client.deploy_certificate,
|
||||
["foo.bar"], "key", "cert", "chain", "fullchain")
|
||||
installer.recovery_routine.assert_called_once_with()
|
||||
|
||||
def test_deploy_certificate_save_failure(self):
|
||||
installer = mock.MagicMock()
|
||||
self.client.installer = installer
|
||||
|
||||
installer.save.side_effect = errors.PluginError
|
||||
self.assertRaises(errors.PluginError, self.client.deploy_certificate,
|
||||
["foo.bar"], "key", "cert", "chain", "fullchain")
|
||||
installer.recovery_routine.assert_called_once_with()
|
||||
|
||||
@mock.patch("letsencrypt.client.zope.component.getUtility")
|
||||
def test_deploy_certificate_restart_failure(self, mock_get_utility):
|
||||
installer = mock.MagicMock()
|
||||
installer.restart.side_effect = [errors.PluginError, None]
|
||||
self.client.installer = installer
|
||||
|
||||
self.assertRaises(errors.PluginError, self.client.deploy_certificate,
|
||||
["foo.bar"], "key", "cert", "chain", "fullchain")
|
||||
self.assertEqual(mock_get_utility().add_message.call_count, 1)
|
||||
installer.rollback_checkpoints.assert_called_once_with()
|
||||
self.assertEqual(installer.restart.call_count, 2)
|
||||
|
||||
def test_deploy_certificate_restart_failure_without_recovery(self):
|
||||
@mock.patch("letsencrypt.client.zope.component.getUtility")
|
||||
def test_deploy_certificate_restart_failure2(self, mock_get_utility):
|
||||
installer = mock.MagicMock()
|
||||
installer.restart.side_effect = errors.PluginError
|
||||
installer.rollback_checkpoints.side_effect = errors.ReverterError
|
||||
@@ -184,6 +205,7 @@ class ClientTest(unittest.TestCase):
|
||||
|
||||
self.assertRaises(errors.PluginError, self.client.deploy_certificate,
|
||||
["foo.bar"], "key", "cert", "chain", "fullchain")
|
||||
self.assertEqual(mock_get_utility().add_message.call_count, 1)
|
||||
installer.rollback_checkpoints.assert_called_once_with()
|
||||
self.assertEqual(installer.restart.call_count, 1)
|
||||
|
||||
@@ -201,10 +223,68 @@ class ClientTest(unittest.TestCase):
|
||||
self.assertEqual(installer.save.call_count, 1)
|
||||
installer.restart.assert_called_once_with()
|
||||
|
||||
def test_enhance_config_no_installer(self):
|
||||
self.assertRaises(errors.Error,
|
||||
self.client.enhance_config, ["foo.bar"])
|
||||
|
||||
@mock.patch("letsencrypt.client.zope.component.getUtility")
|
||||
@mock.patch("letsencrypt.client.enhancements")
|
||||
def test_enhance_config_enhance_failure(self, mock_enhancements,
|
||||
mock_get_utility):
|
||||
mock_enhancements.ask.return_value = True
|
||||
installer = mock.MagicMock()
|
||||
self.client.installer = installer
|
||||
installer.enhance.side_effect = errors.PluginError
|
||||
|
||||
self.assertRaises(errors.PluginError,
|
||||
self.client.enhance_config, ["foo.bar"], True)
|
||||
installer.recovery_routine.assert_called_once_with()
|
||||
self.assertEqual(mock_get_utility().add_message.call_count, 1)
|
||||
|
||||
@mock.patch("letsencrypt.client.zope.component.getUtility")
|
||||
@mock.patch("letsencrypt.client.enhancements")
|
||||
def test_enhance_config_save_failure(self, mock_enhancements,
|
||||
mock_get_utility):
|
||||
mock_enhancements.ask.return_value = True
|
||||
installer = mock.MagicMock()
|
||||
self.client.installer = installer
|
||||
installer.save.side_effect = errors.PluginError
|
||||
|
||||
self.assertRaises(errors.PluginError,
|
||||
self.client.enhance_config, ["foo.bar"], True)
|
||||
installer.recovery_routine.assert_called_once_with()
|
||||
self.assertEqual(mock_get_utility().add_message.call_count, 1)
|
||||
|
||||
@mock.patch("letsencrypt.client.zope.component.getUtility")
|
||||
@mock.patch("letsencrypt.client.enhancements")
|
||||
def test_enhance_config_restart_failure(self, mock_enhancements,
|
||||
mock_get_utility):
|
||||
mock_enhancements.ask.return_value = True
|
||||
installer = mock.MagicMock()
|
||||
self.client.installer = installer
|
||||
installer.restart.side_effect = [errors.PluginError, None]
|
||||
|
||||
self.assertRaises(errors.PluginError,
|
||||
self.client.enhance_config, ["foo.bar"], True)
|
||||
self.assertEqual(mock_get_utility().add_message.call_count, 1)
|
||||
installer.rollback_checkpoints.assert_called_once_with()
|
||||
self.assertEqual(installer.restart.call_count, 2)
|
||||
|
||||
@mock.patch("letsencrypt.client.zope.component.getUtility")
|
||||
@mock.patch("letsencrypt.client.enhancements")
|
||||
def test_enhance_config_restart_failure2(self, mock_enhancements,
|
||||
mock_get_utility):
|
||||
mock_enhancements.ask.return_value = True
|
||||
installer = mock.MagicMock()
|
||||
self.client.installer = installer
|
||||
installer.restart.side_effect = errors.PluginError
|
||||
installer.rollback_checkpoints.side_effect = errors.ReverterError
|
||||
|
||||
self.assertRaises(errors.PluginError,
|
||||
self.client.enhance_config, ["foo.bar"], True)
|
||||
self.assertEqual(mock_get_utility().add_message.call_count, 1)
|
||||
installer.rollback_checkpoints.assert_called_once_with()
|
||||
self.assertEqual(installer.restart.call_count, 1)
|
||||
|
||||
|
||||
class RollbackTest(unittest.TestCase):
|
||||
|
||||
@@ -14,12 +14,12 @@ class NamespaceConfigTest(unittest.TestCase):
|
||||
self.namespace = mock.MagicMock(
|
||||
config_dir='/tmp/config', work_dir='/tmp/foo', foo='bar',
|
||||
server='https://acme-server.org:443/new',
|
||||
dvsni_port=1234, http01_port=4321)
|
||||
tls_sni_01_port=1234, http01_port=4321)
|
||||
from letsencrypt.configuration import NamespaceConfig
|
||||
self.config = NamespaceConfig(self.namespace)
|
||||
|
||||
def test_init_same_ports(self):
|
||||
self.namespace.dvsni_port = 4321
|
||||
self.namespace.tls_sni_01_port = 4321
|
||||
from letsencrypt.configuration import NamespaceConfig
|
||||
self.assertRaises(errors.Error, NamespaceConfig, self.namespace)
|
||||
|
||||
@@ -59,6 +59,38 @@ class NamespaceConfigTest(unittest.TestCase):
|
||||
self.namespace.http01_port = None
|
||||
self.assertEqual(80, self.config.http01_port)
|
||||
|
||||
def test_absolute_paths(self):
|
||||
from letsencrypt.configuration import NamespaceConfig
|
||||
|
||||
config_base = "foo"
|
||||
work_base = "bar"
|
||||
logs_base = "baz"
|
||||
|
||||
mock_namespace = mock.MagicMock(spec=['config_dir', 'work_dir',
|
||||
'logs_dir', 'http01_port',
|
||||
'tls_sni_01_port',
|
||||
'domains', 'server'])
|
||||
mock_namespace.config_dir = config_base
|
||||
mock_namespace.work_dir = work_base
|
||||
mock_namespace.logs_dir = logs_base
|
||||
config = NamespaceConfig(mock_namespace)
|
||||
|
||||
self.assertTrue(os.path.isabs(config.config_dir))
|
||||
self.assertEqual(config.config_dir,
|
||||
os.path.join(os.getcwd(), config_base))
|
||||
self.assertTrue(os.path.isabs(config.work_dir))
|
||||
self.assertEqual(config.work_dir,
|
||||
os.path.join(os.getcwd(), work_base))
|
||||
self.assertTrue(os.path.isabs(config.logs_dir))
|
||||
self.assertEqual(config.logs_dir,
|
||||
os.path.join(os.getcwd(), logs_base))
|
||||
self.assertTrue(os.path.isabs(config.accounts_dir))
|
||||
self.assertTrue(os.path.isabs(config.backup_dir))
|
||||
self.assertTrue(os.path.isabs(config.csr_dir))
|
||||
self.assertTrue(os.path.isabs(config.in_progress_dir))
|
||||
self.assertTrue(os.path.isabs(config.key_dir))
|
||||
self.assertTrue(os.path.isabs(config.temp_checkpoint_dir))
|
||||
|
||||
|
||||
class RenewerConfigurationTest(unittest.TestCase):
|
||||
"""Test for letsencrypt.configuration.RenewerConfiguration."""
|
||||
@@ -81,6 +113,28 @@ class RenewerConfigurationTest(unittest.TestCase):
|
||||
self.config.renewal_configs_dir, '/tmp/config/renewal_configs')
|
||||
self.assertEqual(self.config.renewer_config_file, '/tmp/config/r.conf')
|
||||
|
||||
def test_absolute_paths(self):
|
||||
from letsencrypt.configuration import NamespaceConfig
|
||||
from letsencrypt.configuration import RenewerConfiguration
|
||||
|
||||
config_base = "foo"
|
||||
work_base = "bar"
|
||||
logs_base = "baz"
|
||||
|
||||
mock_namespace = mock.MagicMock(spec=['config_dir', 'work_dir',
|
||||
'logs_dir', 'http01_port',
|
||||
'tls_sni_01_port',
|
||||
'domains', 'server'])
|
||||
mock_namespace.config_dir = config_base
|
||||
mock_namespace.work_dir = work_base
|
||||
mock_namespace.logs_dir = logs_base
|
||||
config = RenewerConfiguration(NamespaceConfig(mock_namespace))
|
||||
|
||||
self.assertTrue(os.path.isabs(config.archive_dir))
|
||||
self.assertTrue(os.path.isabs(config.live_dir))
|
||||
self.assertTrue(os.path.isabs(config.renewal_configs_dir))
|
||||
self.assertTrue(os.path.isabs(config.renewer_config_file))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main() # pragma: no cover
|
||||
|
||||
@@ -34,7 +34,7 @@ class PerformTest(unittest.TestCase):
|
||||
def test_unexpected(self):
|
||||
self.assertRaises(
|
||||
errors.ContAuthError, self.auth.perform, [
|
||||
achallenges.DVSNI(
|
||||
achallenges.KeyAuthorizationAnnotatedChallenge(
|
||||
challb=None, domain="0", account_key="invalid_key")])
|
||||
|
||||
def test_chall_pref(self):
|
||||
@@ -53,7 +53,7 @@ class CleanupTest(unittest.TestCase):
|
||||
mock.MagicMock(server="demo_server.org"), None)
|
||||
|
||||
def test_unexpected(self):
|
||||
unexpected = achallenges.DVSNI(
|
||||
unexpected = achallenges.KeyAuthorizationAnnotatedChallenge(
|
||||
challb=None, domain="0", account_key="dummy_key")
|
||||
self.assertRaises(errors.ContAuthError, self.auth.cleanup, [unexpected])
|
||||
|
||||
|
||||
@@ -13,7 +13,11 @@ class ErrorHandlerTest(unittest.TestCase):
|
||||
from letsencrypt import error_handler
|
||||
|
||||
self.init_func = mock.MagicMock()
|
||||
self.handler = error_handler.ErrorHandler(self.init_func)
|
||||
self.init_args = set((42,))
|
||||
self.init_kwargs = {'foo': 'bar'}
|
||||
self.handler = error_handler.ErrorHandler(self.init_func,
|
||||
*self.init_args,
|
||||
**self.init_kwargs)
|
||||
# pylint: disable=protected-access
|
||||
self.signals = error_handler._SIGNALS
|
||||
|
||||
@@ -23,7 +27,8 @@ class ErrorHandlerTest(unittest.TestCase):
|
||||
raise ValueError
|
||||
except ValueError:
|
||||
pass
|
||||
self.init_func.assert_called_once_with()
|
||||
self.init_func.assert_called_once_with(*self.init_args,
|
||||
**self.init_kwargs)
|
||||
|
||||
@mock.patch('letsencrypt.error_handler.os')
|
||||
@mock.patch('letsencrypt.error_handler.signal')
|
||||
@@ -37,7 +42,8 @@ class ErrorHandlerTest(unittest.TestCase):
|
||||
|
||||
signum = self.signals[0]
|
||||
signal_handler(signum, None)
|
||||
self.init_func.assert_called_once_with()
|
||||
self.init_func.assert_called_once_with(*self.init_args,
|
||||
**self.init_kwargs)
|
||||
mock_os.kill.assert_called_once_with(mock_os.getpid(), signum)
|
||||
|
||||
self.handler.reset_signal_handlers()
|
||||
@@ -48,7 +54,8 @@ class ErrorHandlerTest(unittest.TestCase):
|
||||
bad_func = mock.MagicMock(side_effect=[ValueError])
|
||||
self.handler.register(bad_func)
|
||||
self.handler.call_registered()
|
||||
self.init_func.assert_called_once_with()
|
||||
self.init_func.assert_called_once_with(*self.init_args,
|
||||
**self.init_kwargs)
|
||||
bad_func.assert_called_once_with()
|
||||
|
||||
def test_sysexit_ignored(self):
|
||||
|
||||
@@ -688,9 +688,13 @@ class RenewableCertTests(BaseRenewableCertTest):
|
||||
self.test_rc.configfile["renewalparams"]["rsa_key_size"] = "2048"
|
||||
self.test_rc.configfile["renewalparams"]["server"] = "acme.example.com"
|
||||
self.test_rc.configfile["renewalparams"]["authenticator"] = "fake"
|
||||
self.test_rc.configfile["renewalparams"]["dvsni_port"] = "4430"
|
||||
self.test_rc.configfile["renewalparams"]["tls_sni_01_port"] = "4430"
|
||||
self.test_rc.configfile["renewalparams"]["http01_port"] = "1234"
|
||||
self.test_rc.configfile["renewalparams"]["account"] = "abcde"
|
||||
self.test_rc.configfile["renewalparams"]["domains"] = ["example.com"]
|
||||
self.test_rc.configfile["renewalparams"]["config_dir"] = "config"
|
||||
self.test_rc.configfile["renewalparams"]["work_dir"] = "work"
|
||||
self.test_rc.configfile["renewalparams"]["logs_dir"] = "logs"
|
||||
mock_auth = mock.MagicMock()
|
||||
mock_pd.PluginsRegistry.find_all.return_value = {"apache": mock_auth}
|
||||
# Fails because "fake" != "apache"
|
||||
|
||||
@@ -225,25 +225,25 @@ htmlhelp_basename = 'letshelp-letsencryptdoc'
|
||||
# -- Options for LaTeX output ---------------------------------------------
|
||||
|
||||
latex_elements = {
|
||||
# The paper size ('letterpaper' or 'a4paper').
|
||||
#'papersize': 'letterpaper',
|
||||
# The paper size ('letterpaper' or 'a4paper').
|
||||
#'papersize': 'letterpaper',
|
||||
|
||||
# The font size ('10pt', '11pt' or '12pt').
|
||||
#'pointsize': '10pt',
|
||||
# The font size ('10pt', '11pt' or '12pt').
|
||||
#'pointsize': '10pt',
|
||||
|
||||
# Additional stuff for the LaTeX preamble.
|
||||
#'preamble': '',
|
||||
# Additional stuff for the LaTeX preamble.
|
||||
#'preamble': '',
|
||||
|
||||
# Latex figure (float) alignment
|
||||
#'figure_align': 'htbp',
|
||||
# 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, 'letshelp-letsencrypt.tex', u'letshelp-letsencrypt Documentation',
|
||||
u'Let\'s Encrypt Project', 'manual'),
|
||||
(master_doc, 'letshelp-letsencrypt.tex', u'letshelp-letsencrypt Documentation',
|
||||
u'Let\'s Encrypt Project', 'manual'),
|
||||
]
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top of
|
||||
@@ -286,9 +286,9 @@ man_pages = [
|
||||
# (source start file, target name, title, author,
|
||||
# dir menu entry, description, category)
|
||||
texinfo_documents = [
|
||||
(master_doc, 'letshelp-letsencrypt', u'letshelp-letsencrypt Documentation',
|
||||
author, 'letshelp-letsencrypt', 'One line description of project.',
|
||||
'Miscellaneous'),
|
||||
(master_doc, 'letshelp-letsencrypt', u'letshelp-letsencrypt Documentation',
|
||||
author, 'letshelp-letsencrypt', 'One line description of project.',
|
||||
'Miscellaneous'),
|
||||
]
|
||||
|
||||
# Documents to append as an appendix to all manuals.
|
||||
|
||||
@@ -27,7 +27,7 @@ common() {
|
||||
"$@"
|
||||
}
|
||||
|
||||
common --domains le1.wtf --standalone-supported-challenges dvsni auth
|
||||
common --domains le1.wtf --standalone-supported-challenges tls-sni-01 auth
|
||||
common --domains le2.wtf --standalone-supported-challenges http-01 run
|
||||
common -a manual -d le.wtf auth
|
||||
|
||||
@@ -40,7 +40,7 @@ common auth --csr "$CSR_PATH" \
|
||||
openssl x509 -in "${root}/csr/0000_cert.pem" -text
|
||||
openssl x509 -in "${root}/csr/0000_chain.pem" -text
|
||||
|
||||
common --domain le3.wtf install \
|
||||
common --domains le3.wtf install \
|
||||
--cert-path "${root}/csr/cert.pem" \
|
||||
--key-path "${root}/csr/key.pem"
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ letsencrypt_test () {
|
||||
letsencrypt \
|
||||
--server "${SERVER:-http://localhost:4000/directory}" \
|
||||
--no-verify-ssl \
|
||||
--dvsni-port 5001 \
|
||||
--tls-sni-01-port 5001 \
|
||||
--http-01-port 5002 \
|
||||
--manual-test-mode \
|
||||
$store_flags \
|
||||
|
||||
Reference in New Issue
Block a user