mirror of
https://github.com/certbot/certbot.git
synced 2026-06-03 22:08:07 -04:00
This commit is contained in:
commit
9d88102b3f
189 changed files with 7428 additions and 4893 deletions
46
.travis.yml
46
.travis.yml
|
|
@ -4,9 +4,6 @@ cache:
|
|||
directories:
|
||||
- $HOME/.cache/pip
|
||||
|
||||
# This makes sure we get a host with docker-compose present.
|
||||
dist: trusty
|
||||
|
||||
before_install:
|
||||
- 'dpkg -s libaugeas0'
|
||||
|
||||
|
|
@ -20,43 +17,64 @@ env:
|
|||
matrix:
|
||||
include:
|
||||
- python: "2.7"
|
||||
env: TOXENV=cover BOULDER_INTEGRATION=1
|
||||
sudo: true
|
||||
after_failure:
|
||||
- sudo cat /var/log/mysql/error.log
|
||||
- ps aux | grep mysql
|
||||
env: TOXENV=cover
|
||||
- python: "2.7"
|
||||
env: TOXENV=lint
|
||||
- python: "2.7"
|
||||
env: TOXENV=py27-oldest BOULDER_INTEGRATION=1
|
||||
sudo: true
|
||||
sudo: required
|
||||
after_failure:
|
||||
- sudo cat /var/log/mysql/error.log
|
||||
- ps aux | grep mysql
|
||||
services: docker
|
||||
- python: "2.6"
|
||||
env: TOXENV=py26 BOULDER_INTEGRATION=1
|
||||
sudo: true
|
||||
sudo: required
|
||||
after_failure:
|
||||
- sudo cat /var/log/mysql/error.log
|
||||
- ps aux | grep mysql
|
||||
services: docker
|
||||
- python: "2.6"
|
||||
env: TOXENV=py26-oldest BOULDER_INTEGRATION=1
|
||||
sudo: true
|
||||
sudo: required
|
||||
after_failure:
|
||||
- sudo cat /var/log/mysql/error.log
|
||||
- ps aux | grep mysql
|
||||
services: docker
|
||||
- python: "2.7"
|
||||
env: TOXENV=py27_install BOULDER_INTEGRATION=1
|
||||
sudo: required
|
||||
after_failure:
|
||||
- sudo cat /var/log/mysql/error.log
|
||||
- ps aux | grep mysql
|
||||
services: docker
|
||||
- sudo: required
|
||||
env: TOXENV=apache_compat
|
||||
services: docker
|
||||
before_install:
|
||||
addons:
|
||||
- sudo: required
|
||||
env: TOXENV=nginx_compat
|
||||
services: docker
|
||||
before_install:
|
||||
addons:
|
||||
- sudo: required
|
||||
env: TOXENV=le_auto
|
||||
env: TOXENV=le_auto_precise
|
||||
services: docker
|
||||
before_install:
|
||||
addons:
|
||||
- sudo: required
|
||||
env: TOXENV=apache_compat
|
||||
env: TOXENV=le_auto_trusty
|
||||
services: docker
|
||||
before_install:
|
||||
addons:
|
||||
- sudo: required
|
||||
env: TOXENV=le_auto_wheezy
|
||||
services: docker
|
||||
before_install:
|
||||
addons:
|
||||
- sudo: required
|
||||
env: TOXENV=le_auto_centos6
|
||||
services: docker
|
||||
before_install:
|
||||
addons:
|
||||
|
|
@ -103,7 +121,6 @@ addons:
|
|||
- python-dev
|
||||
- python-virtualenv
|
||||
- gcc
|
||||
- dialog
|
||||
- libaugeas0
|
||||
- libssl-dev
|
||||
- libffi-dev
|
||||
|
|
@ -115,7 +132,6 @@ addons:
|
|||
- apache2
|
||||
- libapache2-mod-wsgi
|
||||
- libapache2-mod-macro
|
||||
- sudo
|
||||
|
||||
install: "travis_retry pip install tox coveralls"
|
||||
script: 'travis_retry tox && ([ "xxx$BOULDER_INTEGRATION" = "xxx" ] || ./tests/travis-integration.sh)'
|
||||
|
|
|
|||
1
Vagrantfile
vendored
1
Vagrantfile
vendored
|
|
@ -29,6 +29,7 @@ Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
|
|||
# VM needs more memory to run test suite, got "OSError: [Errno 12]
|
||||
# Cannot allocate memory" when running
|
||||
# letsencrypt.client.tests.display.util_test.NcursesDisplayTest
|
||||
# We may no longer need this.
|
||||
v.memory = 1024
|
||||
|
||||
# Handle cases when the host is behind a private network by making the
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
"""ACME client API."""
|
||||
import base64
|
||||
import collections
|
||||
import datetime
|
||||
from email.utils import parsedate_tz
|
||||
|
|
@ -26,7 +27,13 @@ logger = logging.getLogger(__name__)
|
|||
# for SSL, which does allow these options to be configured.
|
||||
# https://urllib3.readthedocs.org/en/latest/security.html#insecureplatformwarning
|
||||
if sys.version_info < (2, 7, 9): # pragma: no cover
|
||||
requests.packages.urllib3.contrib.pyopenssl.inject_into_urllib3()
|
||||
try:
|
||||
requests.packages.urllib3.contrib.pyopenssl.inject_into_urllib3()
|
||||
except AttributeError:
|
||||
import urllib3.contrib.pyopenssl # pylint: disable=import-error
|
||||
urllib3.contrib.pyopenssl.inject_into_urllib3()
|
||||
|
||||
DER_CONTENT_TYPE = 'application/pkix-cert'
|
||||
|
||||
|
||||
class Client(object): # pylint: disable=too-many-instance-attributes
|
||||
|
|
@ -45,7 +52,6 @@ class Client(object): # pylint: disable=too-many-instance-attributes
|
|||
`verify_ssl`.
|
||||
|
||||
"""
|
||||
DER_CONTENT_TYPE = 'application/pkix-cert'
|
||||
|
||||
def __init__(self, directory, key, alg=jose.RS256, verify_ssl=True,
|
||||
net=None):
|
||||
|
|
@ -304,7 +310,7 @@ class Client(object): # pylint: disable=too-many-instance-attributes
|
|||
# TODO: assert len(authzrs) == number of SANs
|
||||
req = messages.CertificateRequest(csr=csr)
|
||||
|
||||
content_type = self.DER_CONTENT_TYPE # TODO: add 'cert_type 'argument
|
||||
content_type = DER_CONTENT_TYPE # TODO: add 'cert_type 'argument
|
||||
response = self.net.post(
|
||||
authzrs[0].new_cert_uri, # TODO: acme-spec #90
|
||||
req,
|
||||
|
|
@ -406,7 +412,7 @@ class Client(object): # pylint: disable=too-many-instance-attributes
|
|||
:rtype: tuple
|
||||
|
||||
"""
|
||||
content_type = self.DER_CONTENT_TYPE # TODO: make it a param
|
||||
content_type = DER_CONTENT_TYPE # TODO: make it a param
|
||||
response = self.net.get(uri, headers={'Accept': content_type},
|
||||
content_type=content_type)
|
||||
return response, jose.ComparableX509(OpenSSL.crypto.load_certificate(
|
||||
|
|
@ -475,17 +481,21 @@ class Client(object): # pylint: disable=too-many-instance-attributes
|
|||
"Recursion limit reached. Didn't get {0}".format(uri))
|
||||
return chain
|
||||
|
||||
def revoke(self, cert):
|
||||
def revoke(self, cert, rsn):
|
||||
"""Revoke certificate.
|
||||
|
||||
:param .ComparableX509 cert: `OpenSSL.crypto.X509` wrapped in
|
||||
`.ComparableX509`
|
||||
|
||||
:param int rsn: Reason code for certificate revocation.
|
||||
|
||||
:raises .ClientError: If revocation is unsuccessful.
|
||||
|
||||
"""
|
||||
response = self.net.post(self.directory[messages.Revocation],
|
||||
messages.Revocation(certificate=cert),
|
||||
messages.Revocation(
|
||||
certificate=cert,
|
||||
reason=rsn),
|
||||
content_type=None)
|
||||
if response.status_code != http_client.OK:
|
||||
raise errors.ClientError(
|
||||
|
|
@ -521,10 +531,11 @@ class ClientNetwork(object): # pylint: disable=too-many-instance-attributes
|
|||
:rtype: `.JWS`
|
||||
|
||||
"""
|
||||
jobj = obj.json_dumps().encode()
|
||||
logger.debug('Serialized JSON: %s', jobj)
|
||||
jobj = obj.json_dumps(indent=2).encode()
|
||||
logger.debug('JWS payload:\n%s', jobj)
|
||||
return jws.JWS.sign(
|
||||
payload=jobj, key=self.key, alg=self.alg, nonce=nonce).json_dumps()
|
||||
payload=jobj, key=self.key, alg=self.alg,
|
||||
nonce=nonce).json_dumps(indent=2)
|
||||
|
||||
@classmethod
|
||||
def _check_response(cls, response, content_type=None):
|
||||
|
|
@ -545,9 +556,6 @@ class ClientNetwork(object): # pylint: disable=too-many-instance-attributes
|
|||
:raises .ClientError: In case of other networking errors.
|
||||
|
||||
"""
|
||||
logger.debug('Received response %s (headers: %s): %r',
|
||||
response, response.headers, response.content)
|
||||
|
||||
response_ct = response.headers.get('Content-Type')
|
||||
try:
|
||||
# TODO: response.json() is called twice, once here, and
|
||||
|
|
@ -599,14 +607,26 @@ class ClientNetwork(object): # pylint: disable=too-many-instance-attributes
|
|||
|
||||
|
||||
"""
|
||||
logging.debug('Sending %s request to %s. args: %r, kwargs: %r',
|
||||
method, url, args, kwargs)
|
||||
if method == "POST":
|
||||
logging.debug('Sending POST request to %s:\n%s',
|
||||
url, kwargs['data'])
|
||||
else:
|
||||
logging.debug('Sending %s request to %s.', method, url)
|
||||
kwargs['verify'] = self.verify_ssl
|
||||
kwargs.setdefault('headers', {})
|
||||
kwargs['headers'].setdefault('User-Agent', self.user_agent)
|
||||
response = self.session.request(method, url, *args, **kwargs)
|
||||
logging.debug('Received %s. Headers: %s. Content: %r',
|
||||
response, response.headers, response.content)
|
||||
# If content is DER, log the base64 of it instead of raw bytes, to keep
|
||||
# binary data out of the logs.
|
||||
if response.headers.get("Content-Type") == DER_CONTENT_TYPE:
|
||||
debug_content = base64.b64encode(response.content)
|
||||
else:
|
||||
debug_content = response.content
|
||||
logger.debug('Received response:\nHTTP %d\n%s\n\n%s',
|
||||
response.status_code,
|
||||
"\n".join(["{0}: {1}".format(k, v)
|
||||
for k, v in response.headers.items()]),
|
||||
debug_content)
|
||||
return response
|
||||
|
||||
def head(self, *args, **kwargs):
|
||||
|
|
@ -631,7 +651,7 @@ class ClientNetwork(object): # pylint: disable=too-many-instance-attributes
|
|||
decoded_nonce = jws.Header._fields['nonce'].decode(nonce)
|
||||
except jose.DeserializationError as error:
|
||||
raise errors.BadNonce(nonce, error)
|
||||
logger.debug('Storing nonce: %r', decoded_nonce)
|
||||
logger.debug('Storing nonce: %s', nonce)
|
||||
self._nonces.add(decoded_nonce)
|
||||
else:
|
||||
raise errors.MissingNonce(response)
|
||||
|
|
|
|||
|
|
@ -81,6 +81,9 @@ class ClientTest(unittest.TestCase):
|
|||
uri='https://www.letsencrypt-demo.org/acme/cert/1',
|
||||
cert_chain_uri='https://www.letsencrypt-demo.org/ca')
|
||||
|
||||
# Reason code for revocation
|
||||
self.rsn = 1
|
||||
|
||||
def test_init_downloads_directory(self):
|
||||
uri = 'http://www.letsencrypt-demo.org/directory'
|
||||
from acme.client import Client
|
||||
|
|
@ -427,13 +430,22 @@ class ClientTest(unittest.TestCase):
|
|||
self.assertRaises(errors.Error, self.client.fetch_chain, self.certr)
|
||||
|
||||
def test_revoke(self):
|
||||
self.client.revoke(self.certr.body)
|
||||
self.client.revoke(self.certr.body, self.rsn)
|
||||
self.net.post.assert_called_once_with(
|
||||
self.directory[messages.Revocation], mock.ANY, content_type=None)
|
||||
|
||||
def test_revocation_payload(self):
|
||||
obj = messages.Revocation(certificate=self.certr.body, reason=self.rsn)
|
||||
self.assertTrue('reason' in obj.to_partial_json().keys())
|
||||
self.assertEquals(self.rsn, obj.to_partial_json()['reason'])
|
||||
|
||||
def test_revoke_bad_status_raises_error(self):
|
||||
self.response.status_code = http_client.METHOD_NOT_ALLOWED
|
||||
self.assertRaises(errors.ClientError, self.client.revoke, self.certr)
|
||||
self.assertRaises(
|
||||
errors.ClientError,
|
||||
self.client.revoke,
|
||||
self.certr,
|
||||
self.rsn)
|
||||
|
||||
|
||||
class ClientNetworkTest(unittest.TestCase):
|
||||
|
|
@ -534,6 +546,29 @@ class ClientNetworkTest(unittest.TestCase):
|
|||
'HEAD', 'http://example.com/', 'foo',
|
||||
headers=mock.ANY, verify=mock.ANY, bar='baz')
|
||||
|
||||
@mock.patch('acme.client.logger')
|
||||
def test_send_request_get_der(self, mock_logger):
|
||||
self.net.session = mock.MagicMock()
|
||||
self.net.session.request.return_value = mock.MagicMock(
|
||||
ok=True, status_code=http_client.OK,
|
||||
headers={"Content-Type": "application/pkix-cert"},
|
||||
content=b"hi")
|
||||
# pylint: disable=protected-access
|
||||
self.net._send_request('HEAD', 'http://example.com/', 'foo', bar='baz')
|
||||
mock_logger.debug.assert_called_once_with(
|
||||
'Received response:\nHTTP %d\n%s\n\n%s', 200,
|
||||
'Content-Type: application/pkix-cert', b'aGk=')
|
||||
|
||||
def test_send_request_post(self):
|
||||
self.net.session = mock.MagicMock()
|
||||
self.net.session.request.return_value = self.response
|
||||
# pylint: disable=protected-access
|
||||
self.assertEqual(self.response, self.net._send_request(
|
||||
'POST', 'http://example.com/', 'foo', data='qux', bar='baz'))
|
||||
self.net.session.request.assert_called_once_with(
|
||||
'POST', 'http://example.com/', 'foo',
|
||||
headers=mock.ANY, verify=mock.ANY, data='qux', bar='baz')
|
||||
|
||||
def test_send_request_verify_ssl(self):
|
||||
# pylint: disable=protected-access
|
||||
for verify in True, False:
|
||||
|
|
|
|||
|
|
@ -63,6 +63,8 @@ class SSLSocket(object): # pylint: disable=too-few-public-methods
|
|||
server_name)
|
||||
return
|
||||
new_context = OpenSSL.SSL.Context(self.method)
|
||||
new_context.set_options(OpenSSL.SSL.OP_NO_SSLv2)
|
||||
new_context.set_options(OpenSSL.SSL.OP_NO_SSLv3)
|
||||
new_context.use_privatekey(key)
|
||||
new_context.use_certificate(cert)
|
||||
connection.set_context(new_context)
|
||||
|
|
@ -86,6 +88,8 @@ class SSLSocket(object): # pylint: disable=too-few-public-methods
|
|||
sock, addr = self.sock.accept()
|
||||
|
||||
context = OpenSSL.SSL.Context(self.method)
|
||||
context.set_options(OpenSSL.SSL.OP_NO_SSLv2)
|
||||
context.set_options(OpenSSL.SSL.OP_NO_SSLv3)
|
||||
context.set_tlsext_servername_callback(self._pick_certificate_cb)
|
||||
|
||||
ssl_sock = self.FakeConnection(OpenSSL.SSL.Connection(context, sock))
|
||||
|
|
|
|||
|
|
@ -18,9 +18,11 @@ from acme import test_util
|
|||
class SSLSocketAndProbeSNITest(unittest.TestCase):
|
||||
"""Tests for acme.crypto_util.SSLSocket/probe_sni."""
|
||||
|
||||
_multiprocess_can_split_ = True
|
||||
|
||||
def setUp(self):
|
||||
self.cert = test_util.load_comparable_cert('cert.pem')
|
||||
key = test_util.load_pyopenssl_private_key('rsa512_key.pem')
|
||||
self.cert = test_util.load_comparable_cert('rsa2048_cert.pem')
|
||||
key = test_util.load_pyopenssl_private_key('rsa2048_key.pem')
|
||||
# pylint: disable=protected-access
|
||||
certs = {b'foo': (key, self.cert.wrapped)}
|
||||
|
||||
|
|
@ -67,6 +69,8 @@ class SSLSocketAndProbeSNITest(unittest.TestCase):
|
|||
class PyOpenSSLCertOrReqSANTest(unittest.TestCase):
|
||||
"""Test for acme.crypto_util._pyopenssl_cert_or_req_san."""
|
||||
|
||||
_multiprocess_can_split_ = True
|
||||
|
||||
@classmethod
|
||||
def _call(cls, loader, name):
|
||||
# pylint: disable=protected-access
|
||||
|
|
@ -131,6 +135,8 @@ class PyOpenSSLCertOrReqSANTest(unittest.TestCase):
|
|||
class RandomSnTest(unittest.TestCase):
|
||||
"""Test for random certificate serial numbers."""
|
||||
|
||||
_multiprocess_can_split_ = True
|
||||
|
||||
def setUp(self):
|
||||
self.cert_count = 5
|
||||
self.serial_num = []
|
||||
|
|
|
|||
|
|
@ -253,10 +253,6 @@ class JSONObjectWithFields(util.ImmutableMap, interfaces.JSONDeSerializable):
|
|||
raise errors.SerializationError(
|
||||
'Could not encode {0} ({1}): {2}'.format(
|
||||
slot, value, error))
|
||||
if omitted:
|
||||
# pylint: disable=star-args
|
||||
logger.debug('Omitted empty fields: %s', ', '.join(
|
||||
'{0!s}={1!r}'.format(*field) for field in omitted))
|
||||
return jobj
|
||||
|
||||
def to_partial_json(self):
|
||||
|
|
|
|||
|
|
@ -469,3 +469,4 @@ class Revocation(jose.JSONObjectWithFields):
|
|||
resource = fields.Resource(resource_type)
|
||||
certificate = jose.Field(
|
||||
'certificate', decoder=jose.decode_cert, encoder=jose.encode_cert)
|
||||
reason = jose.Field('reason')
|
||||
|
|
|
|||
|
|
@ -21,6 +21,8 @@ from acme import test_util
|
|||
class TLSServerTest(unittest.TestCase):
|
||||
"""Tests for acme.standalone.TLSServer."""
|
||||
|
||||
_multiprocess_can_split_ = True
|
||||
|
||||
def test_bind(self): # pylint: disable=no-self-use
|
||||
from acme.standalone import TLSServer
|
||||
server = TLSServer(
|
||||
|
|
@ -31,10 +33,12 @@ class TLSServerTest(unittest.TestCase):
|
|||
class TLSSNI01ServerTest(unittest.TestCase):
|
||||
"""Test for acme.standalone.TLSSNI01Server."""
|
||||
|
||||
_multiprocess_can_split_ = True
|
||||
|
||||
def setUp(self):
|
||||
self.certs = {b'localhost': (
|
||||
test_util.load_pyopenssl_private_key('rsa512_key.pem'),
|
||||
test_util.load_cert('cert.pem'),
|
||||
test_util.load_pyopenssl_private_key('rsa2048_key.pem'),
|
||||
test_util.load_cert('rsa2048_cert.pem'),
|
||||
)}
|
||||
from acme.standalone import TLSSNI01Server
|
||||
self.server = TLSSNI01Server(("", 0), certs=self.certs)
|
||||
|
|
@ -57,6 +61,8 @@ class TLSSNI01ServerTest(unittest.TestCase):
|
|||
class HTTP01ServerTest(unittest.TestCase):
|
||||
"""Tests for acme.standalone.HTTP01Server."""
|
||||
|
||||
_multiprocess_can_split_ = True
|
||||
|
||||
def setUp(self):
|
||||
self.account_key = jose.JWK.load(
|
||||
test_util.load_vector('rsa1024_key.pem'))
|
||||
|
|
@ -109,13 +115,16 @@ class HTTP01ServerTest(unittest.TestCase):
|
|||
class TestSimpleTLSSNI01Server(unittest.TestCase):
|
||||
"""Tests for acme.standalone.simple_tls_sni_01_server."""
|
||||
|
||||
_multiprocess_can_split_ = True
|
||||
|
||||
def setUp(self):
|
||||
# mirror ../examples/standalone
|
||||
self.test_cwd = tempfile.mkdtemp()
|
||||
localhost_dir = os.path.join(self.test_cwd, 'localhost')
|
||||
os.makedirs(localhost_dir)
|
||||
shutil.copy(test_util.vector_path('cert.pem'), localhost_dir)
|
||||
shutil.copy(test_util.vector_path('rsa512_key.pem'),
|
||||
shutil.copy(test_util.vector_path('rsa2048_cert.pem'),
|
||||
os.path.join(localhost_dir, 'cert.pem'))
|
||||
shutil.copy(test_util.vector_path('rsa2048_key.pem'),
|
||||
os.path.join(localhost_dir, 'key.pem'))
|
||||
|
||||
from acme.standalone import simple_tls_sni_01_server
|
||||
|
|
@ -147,7 +156,8 @@ class TestSimpleTLSSNI01Server(unittest.TestCase):
|
|||
time.sleep(1) # wait until thread starts
|
||||
else:
|
||||
self.assertEqual(jose.ComparableX509(cert),
|
||||
test_util.load_comparable_cert('cert.pem'))
|
||||
test_util.load_comparable_cert(
|
||||
'rsa2048_cert.pem'))
|
||||
break
|
||||
|
||||
|
||||
|
|
|
|||
22
acme/acme/testdata/rsa2048_cert.pem
vendored
Normal file
22
acme/acme/testdata/rsa2048_cert.pem
vendored
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
-----BEGIN CERTIFICATE-----
|
||||
MIIDjjCCAnagAwIBAgIJALVG/VbBb5U7MA0GCSqGSIb3DQEBCwUAMFsxCzAJBgNV
|
||||
BAYTAkFVMQswCQYDVQQIDAJXQTEeMBwGA1UEBwwVVGhlIG1pZGRsZSBvZiBub3do
|
||||
ZXJlMR8wHQYDVQQKDBZDZXJ0Ym90IFRlc3QgQ2VydHMgSW5jMCAXDTE2MTEyODIx
|
||||
MzUzN1oYDzIyOTAwOTEzMjEzNTM3WjBbMQswCQYDVQQGEwJBVTELMAkGA1UECAwC
|
||||
V0ExHjAcBgNVBAcMFVRoZSBtaWRkbGUgb2Ygbm93aGVyZTEfMB0GA1UECgwWQ2Vy
|
||||
dGJvdCBUZXN0IENlcnRzIEluYzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC
|
||||
ggEBANoVT1pdvRUUBOqvm7M2ebLEHV7higUH7qAGUZEkfP6W4YriYVY+IHrH1svN
|
||||
PSa+oPTK7weDNmT11ehWnGyECIM9z2r2Hi9yVV0ycxh4hWQ4Nt8BAKZwCwaXpyWm
|
||||
7Gj6m2EzpSN5Dd67g5YAQBrUUh1+RRbFi9c0Ls/6ZOExMvfg8kqt4c2sXCgH1IFn
|
||||
xvvOjBYop7xh0x3L1Akyax0tw8qgQp/z5mkupmVDNJYPFmbzFPMNyDR61ed6QUTD
|
||||
g7P4UAuFkejLLzFvz5YaO7vC+huaTuPhInAhpzqpr4yU97KIjos2/83Itu/Cv8U1
|
||||
RAeEeRTkh0WjUfltoem/5f8bIdsCAwEAAaNTMFEwHQYDVR0OBBYEFHy+bEYqwvFU
|
||||
uQLTkIfQ36AM2DQiMB8GA1UdIwQYMBaAFHy+bEYqwvFUuQLTkIfQ36AM2DQiMA8G
|
||||
A1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAH3ANVzB59FcunZV/F8T
|
||||
RiCD6/gV7Jc3CswU8N8tVjzMCg2jOdTFF9iYZzNNKQvG13o/n5LkQr/lkKRQkWTx
|
||||
nkE5WZbR7vNqlzXgPa9NBiK5rPjgSt8azPW+Skct3Bj4B3PhTMSpoQ7PsUJ8UeV8
|
||||
kTNR5xrRLt6/mLfRJTXWXBM43GEZi8lL5q0nqz0tPGISADshHMo6ZlUu5Hvfp5v+
|
||||
aonpO4sVS9hGOVxjGNMXYApEUy4jid9jjAfEk6jeELJMbXGLy/botFgIJK/QPe6P
|
||||
AfbdFgtg/qzG7Uy0A1iXXfWdgwmVrhCoGYYWCn4yWCAm894QKtdim87CHSDP0WUf
|
||||
Esg=
|
||||
-----END CERTIFICATE-----
|
||||
55
acme/acme/testdata/rsa2048_key.pem
vendored
55
acme/acme/testdata/rsa2048_key.pem
vendored
|
|
@ -1,27 +1,28 @@
|
|||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEowIBAAKCAQEA8HwZMHeImB/iM8/n8CTCR4KeYQB2gLGO3v8xLms+PWH3Zbxc
|
||||
dVtEn25Y34scIh+iOuEXBcSBalBddLHKBGVN3nCfmpupoLm52xgRG44q9OWODpg4
|
||||
FSi4afqVw2agMx0RHi0v3GVcdpqB83UW42kK1ESZHUuq7mxLg8u3IMYZFm6Amsf+
|
||||
YQjBbDNn8NczJOFhsExP2EdM5ykgM1Om8aqTqqPMgPub68/r4Sym+BjLnvRq5Qtz
|
||||
h/jCfOBIIpAwg3lj7l8OyE3kkD3ALtuiuminNUqLHEkUaLq/Xiv8V8mvnrhG7h3Q
|
||||
+L1Xc707P0dz5YM5XxTMhmUE1cae/lQ0KbNrpwIDAQABAoIBAAiDXCDrGlrIRimv
|
||||
YnaN1pLRfOnSKl/D6VrbjdIm2b0yip9/W4aMBJHgRiUjt4s9s3CCJ1585lftIGHR
|
||||
KWWecHM/aWb/u7GE4Z9v6qsfDUY+GhlKKjIVjvGxfTu9lk446TI4R0l2DR/luFP2
|
||||
ASlrvoZlJ0ZyN0rZapLv0zvFx32Tukd+3rcMmXfHl7aRGMZG1YTKNmBJ4d9iJ6cP
|
||||
HG3fgSzLQMPLNO/20MzbXdREG5FNQtwaMuFnIcVbtMCvc/71lQQEfANMLCUweEed
|
||||
YWGOjgDeh+731nJsopel+2TSTgnf5VhcFrgChZZdqeKvP+HbXjTE2VkWo7BrzoM7
|
||||
xICYBwECgYEA/ZF/JOjZfwIMUdikv8vldJzRMdFryl4NJWnh4NeThNOEpUcpxnyK
|
||||
wyMnnQaGJa51u9EEnzl0sZ2h2ODjD6KFpz6fkWaVRq5SWalVPAoKZGaoPZV3IUOI
|
||||
8Tm0xkXho+A/FUUEcxCLME+3V9EdPfHaVRJOrbfDyxvNhsj4w9F0aAkCgYEA8sp7
|
||||
XTrolOknJGv4Qt1w6gcm5+cMtLaRfi8ZHPHujl2x9eWE8/s2818az7jc0Xr/G4HQ
|
||||
NeU+3Es4BblEckSHmhUZhx26cZgkLSIIDofEtaEc6u8CyWfxsWvn3l4T3kMdeSLC
|
||||
9UoLk59AH2tkMIh8vzV8LSisLJa341lMdgryQi8CgYAlJKr7PSCe+i3Tz2hSsAts
|
||||
iYwbQBIKErzaPihYRzvUuSc1DreP26535y5mUg5UdrnISVXj/Qaa/fw3SLn6EFSD
|
||||
qyi0o9I6CE8H00YpBU+AZYk/fCV3Oe1VaJ6SbKog1zhmZTXBpSq+aO7ybi9aY5MX
|
||||
4xajW8fSeMAifk3yYTwsAQKBgErcEcOCOVpItU/uloKPYpRWFjHktK83p46fmP+q
|
||||
vOJak1d9KExOBfhuN4caucNBSE1D7l3fzE0CSEjDgg41gRYKMW/Ow8DopybfWlqY
|
||||
lBdokNEDVvmgug35dmnC2h9q1DiYdkJJTV57+Lp3U1H/k28lX59Q7h1lb1eDHic7
|
||||
YszzAoGBAOx05dhOiYbzAJSTQu3oBHFn4mTYIqCcDO6cQrEJwPKAq7mAhT0yOk9N
|
||||
CrqRV/1aes665829cyTwcAZl6nqbzHv5XjX5+g6vmooCb4oCkq49rumHjoQdrX8D
|
||||
RR5b+Spkc1jo4rctCcExzSkgo+K5N3oBVYznecje7O7Z0/qiJE/8
|
||||
-----END RSA PRIVATE KEY-----
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDaFU9aXb0VFATq
|
||||
r5uzNnmyxB1e4YoFB+6gBlGRJHz+luGK4mFWPiB6x9bLzT0mvqD0yu8HgzZk9dXo
|
||||
VpxshAiDPc9q9h4vclVdMnMYeIVkODbfAQCmcAsGl6clpuxo+pthM6UjeQ3eu4OW
|
||||
AEAa1FIdfkUWxYvXNC7P+mThMTL34PJKreHNrFwoB9SBZ8b7zowWKKe8YdMdy9QJ
|
||||
MmsdLcPKoEKf8+ZpLqZlQzSWDxZm8xTzDcg0etXnekFEw4Oz+FALhZHoyy8xb8+W
|
||||
Gju7wvobmk7j4SJwIac6qa+MlPeyiI6LNv/NyLbvwr/FNUQHhHkU5IdFo1H5baHp
|
||||
v+X/GyHbAgMBAAECggEAURFe4C68XRuGAF+rN2Fmt+djK6QXlGswb1gp9hRkSpd3
|
||||
3BLvMAoENOAYnsX6l26Bkr3lQRurmrgv/iBEIaqrJ25QrmgzLFwKE4zvcAdNPsYO
|
||||
z7MltLktwBOb1MlKVHPkUqvKFXeoikWWUqphKhgHNmN7900UALmrNTDVU0jgs3fB
|
||||
o35o8d5SjoC52K4wCTjhPyjt4cdbfbziRs2qFhfGdawidRO1xLlDM4tTTW+5yWGK
|
||||
lt0SwyvDVC6XWeNoT3nXyKjXWP7hcYqm0iS7ffL9YzEC2RXNGQUqeR50i9Y0rDdH
|
||||
Vqcr+Rqio2ww68zbDWBpC/jU133BSoHuSE1wstxIkQKBgQDxlEr42WJfgdajbZ1a
|
||||
hUIeLEgvhezLmD1hcYwZuQCLgizmY2ovvmeAH74koCDEsUUQunPYHsRla7wT3q1/
|
||||
IkR1KgJPwESpkQaKuAqxeEAkv7Gn8Lzcn22jCoRCfGA68wKJz2ECFZDc0RDvRrT/
|
||||
9GhiiGUoO47jv9ezrSDO1eu5/QKBgQDnGfYVMNLiA0fy4AxSyY2vdo7vruOFGpRP
|
||||
n94gwxZ+0dQDWHzn3J4rHivxtcyd/MOZv4I8PtYK7tmmjYv1ngQ6sGl4p8bpUtwj
|
||||
9++/B1CyB1W5/VPqMkd+Sj0dbejycME55+F6/r4basPXxBFFCfknjAlVvyvbBhUy
|
||||
ftNpHxZGtwKBgChJM4t2LPqCW3nbgL8ks9b2SX9rVQbKt4m1dsifWmDpb3VoJMAb
|
||||
f4UVRg8ziONkMIFOppzm3JeRNMcXflVSMJpdTA9in9CrN60QbfAUfpXiRc0cz1H3
|
||||
YEAtM8smlKGf/s9efu3rDMJWNv3AC9UXPAUae8wOypBeYKk8+NilQe89AoGAXEA3
|
||||
xFO+CqyGnwQixzVf0qf//NuSRQLMK1DEyc02gJ9gA4niKmgd11Zu8kjBClvo9MnG
|
||||
wifPJ4Qa6+pa8UwHoinjoF9Q/rit2cnSMS5JXxegd+MRCU7SzS3zYXkLYSPzbhsL
|
||||
Hh7sYmNnFA1XW3jUtZ2n6EusxPyTn5mS6MaZDNcCgYBelFKFjNIQ50NbOnm8DewK
|
||||
jUd5OFKowKodlQVcHiF9CVbjvpN8ZPRcBSmqDU4kpT/rmcybVoL6Zfa/zWkw8+Oh
|
||||
QxKb3BYf5vRUMd/RA+/t5KG4ZOIIYB3qoltAYlhVaINukL6cGVG1qvV/ntcsfsn6
|
||||
kmf1UgGFcKrJuXgwEtTVxw==
|
||||
-----END PRIVATE KEY-----
|
||||
|
|
|
|||
|
|
@ -39,7 +39,6 @@ extensions = [
|
|||
'sphinx.ext.todo',
|
||||
'sphinx.ext.coverage',
|
||||
'sphinx.ext.viewcode',
|
||||
'sphinxcontrib.programoutput',
|
||||
]
|
||||
|
||||
autodoc_member_order = 'bysource'
|
||||
|
|
|
|||
8
acme/docs/jws-help.txt
Normal file
8
acme/docs/jws-help.txt
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
usage: jws [-h] [--compact] {sign,verify} ...
|
||||
|
||||
positional arguments:
|
||||
{sign,verify}
|
||||
|
||||
optional arguments:
|
||||
-h, --help show this help message and exit
|
||||
--compact
|
||||
|
|
@ -1 +1 @@
|
|||
.. program-output:: jws --help all
|
||||
.. literalinclude:: ../jws-help.txt
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
../../../acme/testdata/cert.pem
|
||||
../../../acme/testdata/rsa2048_cert.pem
|
||||
|
|
@ -1 +1 @@
|
|||
../../../acme/testdata/rsa512_key.pem
|
||||
../../../acme/testdata/rsa2048_key.pem
|
||||
|
|
@ -4,20 +4,18 @@ from setuptools import setup
|
|||
from setuptools import find_packages
|
||||
|
||||
|
||||
version = '0.10.0.dev0'
|
||||
version = '0.11.0.dev0'
|
||||
|
||||
# Please update tox.ini when modifying dependency version requirements
|
||||
install_requires = [
|
||||
# load_pem_private/public_key (>=0.6)
|
||||
# rsa_recover_prime_factors (>=0.8)
|
||||
'cryptography>=0.8',
|
||||
'ndg-httpsclient', # urllib3 InsecurePlatformWarning (#304)
|
||||
'pyasn1', # urllib3 InsecurePlatformWarning (#304)
|
||||
# Connection.set_tlsext_host_name (>=0.13)
|
||||
'PyOpenSSL>=0.13',
|
||||
'pyrfc3339',
|
||||
'pytz',
|
||||
'requests',
|
||||
'requests[security]>=2.4.1', # security extras added in 2.4.1
|
||||
# For pkg_resources. >=1.0 so pip resolves it to a version cryptography
|
||||
# will tolerate; see #2599:
|
||||
'setuptools>=1.0',
|
||||
|
|
@ -49,7 +47,6 @@ dev_extras = [
|
|||
docs_extras = [
|
||||
'Sphinx>=1.0', # autodoc_member_order = 'bysource', autodoc_default_flags
|
||||
'sphinx_rtd_theme',
|
||||
'sphinxcontrib-programoutput',
|
||||
]
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -68,9 +68,12 @@ class AugeasConfigurator(common.Plugin):
|
|||
# As aug.get may return null
|
||||
if lens_path and lens in lens_path:
|
||||
msg = (
|
||||
"There has been an error in parsing the file (%s): %s",
|
||||
"There has been an error in parsing the file {0} on line {1}: "
|
||||
"{2}".format(
|
||||
# Strip off /augeas/files and /error
|
||||
path[13:len(path) - 6], self.aug.get(path + "/message"))
|
||||
path[13:len(path) - 6],
|
||||
self.aug.get(path + "/line"),
|
||||
self.aug.get(path + "/message")))
|
||||
raise errors.PluginError(msg)
|
||||
|
||||
# TODO: Cleanup this function
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
"""Apache Configuration based off of Augeas Configurator."""
|
||||
# pylint: disable=too-many-lines
|
||||
import filecmp
|
||||
import fnmatch
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
|
|
@ -362,18 +363,24 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
|||
return vhost
|
||||
|
||||
def included_in_wildcard(self, names, target_name):
|
||||
"""Helper function to see if alias is covered by wildcard"""
|
||||
target_name = target_name.split(".")[::-1]
|
||||
wildcards = [domain.split(".")[1:] for domain in
|
||||
names if domain.startswith("*")]
|
||||
for wildcard in wildcards:
|
||||
if len(wildcard) > len(target_name):
|
||||
continue
|
||||
for idx, segment in enumerate(wildcard[::-1]):
|
||||
if segment != target_name[idx]:
|
||||
break
|
||||
else:
|
||||
# https://docs.python.org/2/tutorial/controlflow.html#break-and-continue-statements-and-else-clauses-on-loops
|
||||
"""Is target_name covered by a wildcard?
|
||||
|
||||
:param names: server aliases
|
||||
:type names: `collections.Iterable` of `str`
|
||||
:param str target_name: name to compare with wildcards
|
||||
|
||||
:returns: True if target_name is covered by a wildcard,
|
||||
otherwise, False
|
||||
:rtype: bool
|
||||
|
||||
"""
|
||||
# use lowercase strings because fnmatch can be case sensitive
|
||||
target_name = target_name.lower()
|
||||
for name in names:
|
||||
name = name.lower()
|
||||
# fnmatch treats "[seq]" specially and [ or ] characters aren't
|
||||
# valid in Apache but Apache doesn't error out if they are present
|
||||
if "[" not in name and fnmatch.fnmatch(target_name, name):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
|
@ -463,7 +470,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
|||
zope.component.getUtility(interfaces.IDisplay).notification(
|
||||
"Apache mod_macro seems to be in use in file(s):\n{0}"
|
||||
"\n\nUnfortunately mod_macro is not yet supported".format(
|
||||
"\n ".join(vhost_macro)))
|
||||
"\n ".join(vhost_macro)), force_interactive=True)
|
||||
|
||||
return all_names
|
||||
|
||||
|
|
@ -1012,6 +1019,8 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
|||
self.parser.find_dir("ServerAlias", target_name,
|
||||
start=vh_path, exclude=False)):
|
||||
return
|
||||
if self._has_matching_wildcard(vh_path, target_name):
|
||||
return
|
||||
if not self.parser.find_dir("ServerName", None,
|
||||
start=vh_path, exclude=False):
|
||||
self.parser.add_dir(vh_path, "ServerName", target_name)
|
||||
|
|
@ -1019,6 +1028,22 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
|||
self.parser.add_dir(vh_path, "ServerAlias", target_name)
|
||||
self._add_servernames(vhost)
|
||||
|
||||
def _has_matching_wildcard(self, vh_path, target_name):
|
||||
"""Is target_name already included in a wildcard in the vhost?
|
||||
|
||||
:param str vh_path: Augeas path to the vhost
|
||||
:param str target_name: name to compare with wildcards
|
||||
|
||||
:returns: True if there is a wildcard covering target_name in
|
||||
the vhost in vhost_path, otherwise, False
|
||||
:rtype: bool
|
||||
|
||||
"""
|
||||
matches = self.parser.find_dir(
|
||||
"ServerAlias", start=vh_path, exclude=False)
|
||||
aliases = (self.aug.get(match) for match in matches)
|
||||
return self.included_in_wildcard(aliases, target_name)
|
||||
|
||||
def _add_name_vhost_if_necessary(self, vhost):
|
||||
"""Add NameVirtualHost Directives if necessary for new vhost.
|
||||
|
||||
|
|
@ -1494,38 +1519,6 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
|||
|
||||
return redirects
|
||||
|
||||
def get_all_certs_keys(self):
|
||||
"""Find all existing keys, certs from configuration.
|
||||
|
||||
Retrieve all certs and keys set in VirtualHosts on the Apache server
|
||||
|
||||
:returns: list of tuples with form [(cert, key, path)]
|
||||
cert - str path to certificate file
|
||||
key - str path to associated key file
|
||||
path - File path to configuration file.
|
||||
:rtype: list
|
||||
|
||||
"""
|
||||
c_k = set()
|
||||
|
||||
for vhost in self.vhosts:
|
||||
if vhost.ssl:
|
||||
cert_path = self.parser.find_dir(
|
||||
"SSLCertificateFile", None,
|
||||
start=vhost.path, exclude=False)
|
||||
key_path = self.parser.find_dir(
|
||||
"SSLCertificateKeyFile", None,
|
||||
start=vhost.path, exclude=False)
|
||||
|
||||
if cert_path and key_path:
|
||||
cert = os.path.abspath(self.parser.get_arg(cert_path[-1]))
|
||||
key = os.path.abspath(self.parser.get_arg(key_path[-1]))
|
||||
c_k.add((cert, key, get_file_path(cert_path[-1])))
|
||||
else:
|
||||
logger.warning(
|
||||
"Invalid VirtualHost configuration - %s", vhost.filep)
|
||||
return c_k
|
||||
|
||||
def is_site_enabled(self, avail_fp):
|
||||
"""Checks to see if the given site is enabled.
|
||||
|
||||
|
|
|
|||
|
|
@ -85,7 +85,8 @@ def _vhost_menu(domain, vhosts):
|
|||
"or Address of {0}.{1}Which virtual host would you "
|
||||
"like to choose?\n(note: conf files with multiple "
|
||||
"vhosts are not yet supported)".format(domain, os.linesep),
|
||||
choices, help_label="More Info", ok_label="Select")
|
||||
choices, help_label="More Info",
|
||||
ok_label="Select", force_interactive=True)
|
||||
except errors.MissingCommandlineFlag:
|
||||
msg = ("Encountered vhost ambiguity but unable to ask for user guidance in "
|
||||
"non-interactive mode. Currently Certbot needs each vhost to be "
|
||||
|
|
@ -101,4 +102,4 @@ def _more_info_vhost(vhost):
|
|||
zope.component.getUtility(interfaces.IDisplay).notification(
|
||||
"Virtual Host Information:{0}{1}{0}{2}".format(
|
||||
os.linesep, "-" * (display_util.WIDTH - 4), str(vhost)),
|
||||
height=display_util.HEIGHT)
|
||||
force_interactive=True)
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@ from certbot_apache.tests import util
|
|||
class AugeasConfiguratorTest(util.ApacheTest):
|
||||
"""Test for Augeas Configurator base class."""
|
||||
|
||||
_multiprocess_can_split_ = True
|
||||
|
||||
def setUp(self): # pylint: disable=arguments-differ
|
||||
super(AugeasConfiguratorTest, self).setUp()
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ from certbot import achallenges
|
|||
from certbot import errors
|
||||
|
||||
from certbot.tests import acme_util
|
||||
from certbot.tests import util as certbot_util
|
||||
|
||||
from certbot_apache import configurator
|
||||
from certbot_apache import parser
|
||||
|
|
@ -24,6 +25,8 @@ from certbot_apache.tests import util
|
|||
class MultipleVhostsTest(util.ApacheTest):
|
||||
"""Test two standard well-configured HTTP vhosts."""
|
||||
|
||||
_multiprocess_can_split_ = True
|
||||
|
||||
def setUp(self): # pylint: disable=arguments-differ
|
||||
super(MultipleVhostsTest, self).setUp()
|
||||
|
||||
|
|
@ -95,7 +98,7 @@ class MultipleVhostsTest(util.ApacheTest):
|
|||
# Weak test..
|
||||
ApacheConfigurator.add_parser_arguments(mock.MagicMock())
|
||||
|
||||
@mock.patch("zope.component.getUtility")
|
||||
@certbot_util.patch_get_utility()
|
||||
def test_get_all_names(self, mock_getutility):
|
||||
mock_getutility.notification = mock.MagicMock(return_value=True)
|
||||
names = self.config.get_all_names()
|
||||
|
|
@ -103,7 +106,7 @@ class MultipleVhostsTest(util.ApacheTest):
|
|||
["certbot.demo", "ocspvhost.com", "encryption-example.demo",
|
||||
"ip-172-30-0-17", "*.blue.purple.com"]))
|
||||
|
||||
@mock.patch("zope.component.getUtility")
|
||||
@certbot_util.patch_get_utility()
|
||||
@mock.patch("certbot_apache.configurator.socket.gethostbyaddr")
|
||||
def test_get_all_names_addrs(self, mock_gethost, mock_getutility):
|
||||
mock_gethost.side_effect = [("google.com", "", ""), socket.error]
|
||||
|
|
@ -218,10 +221,6 @@ class MultipleVhostsTest(util.ApacheTest):
|
|||
self.assertRaises(
|
||||
errors.PluginError, self.config.choose_vhost, "none.com")
|
||||
|
||||
def test_choosevhost_select_vhost_with_wildcard(self):
|
||||
chosen_vhost = self.config.choose_vhost("blue.purple.com", temp=True)
|
||||
self.assertEqual(self.vh_truth[6], chosen_vhost)
|
||||
|
||||
def test_findbest_continues_on_short_domain(self):
|
||||
# pylint: disable=protected-access
|
||||
chosen_vhost = self.config._find_best_vhost("purple.com")
|
||||
|
|
@ -775,21 +774,6 @@ class MultipleVhostsTest(util.ApacheTest):
|
|||
self.assertRaises(errors.MisconfigurationError,
|
||||
self.config.config_test)
|
||||
|
||||
def test_get_all_certs_keys(self):
|
||||
c_k = self.config.get_all_certs_keys()
|
||||
self.assertEqual(len(c_k), 3)
|
||||
cert, key, path = next(iter(c_k))
|
||||
self.assertTrue("cert" in cert)
|
||||
self.assertTrue("key" in key)
|
||||
self.assertTrue("default-ssl" in path or "ocsp-ssl" in path)
|
||||
|
||||
def test_get_all_certs_keys_malformed_conf(self):
|
||||
self.config.parser.find_dir = mock.Mock(
|
||||
side_effect=[["path"], [], ["path"], [], ["path"], []])
|
||||
c_k = self.config.get_all_certs_keys()
|
||||
|
||||
self.assertFalse(c_k)
|
||||
|
||||
def test_more_info(self):
|
||||
self.assertTrue(self.config.more_info())
|
||||
|
||||
|
|
@ -1134,7 +1118,7 @@ class MultipleVhostsTest(util.ApacheTest):
|
|||
not_rewriterule = "NotRewriteRule ^ ..."
|
||||
self.assertFalse(self.config._sift_rewrite_rule(not_rewriterule))
|
||||
|
||||
@mock.patch("certbot_apache.configurator.zope.component.getUtility")
|
||||
@certbot_util.patch_get_utility()
|
||||
def test_make_vhost_ssl_with_existing_rewrite_rule(self, mock_get_utility):
|
||||
self.config.parser.modules.add("rewrite_module")
|
||||
|
||||
|
|
@ -1163,7 +1147,7 @@ class MultipleVhostsTest(util.ApacheTest):
|
|||
mock_get_utility().add_message.assert_called_once_with(mock.ANY,
|
||||
|
||||
mock.ANY)
|
||||
@mock.patch("certbot_apache.configurator.zope.component.getUtility")
|
||||
@certbot_util.patch_get_utility()
|
||||
def test_make_vhost_ssl_with_existing_rewrite_conds(self, mock_get_utility):
|
||||
self.config.parser.modules.add("rewrite_module")
|
||||
|
||||
|
|
@ -1256,6 +1240,7 @@ class MultipleVhostsTest(util.ApacheTest):
|
|||
class AugeasVhostsTest(util.ApacheTest):
|
||||
"""Test vhosts with illegal names dependant on augeas version."""
|
||||
# pylint: disable=protected-access
|
||||
_multiprocess_can_split_ = True
|
||||
|
||||
def setUp(self): # pylint: disable=arguments-differ
|
||||
td = "debian_apache_2_4/augeas_vhosts"
|
||||
|
|
@ -1267,8 +1252,6 @@ class AugeasVhostsTest(util.ApacheTest):
|
|||
|
||||
self.config = util.get_apache_configurator(
|
||||
self.config_path, self.vhost_path, self.config_dir, self.work_dir)
|
||||
self.vh_truth = util.get_vh_truth(
|
||||
self.temp_dir, "debian_apache_2_4/augeas_vhosts")
|
||||
|
||||
def tearDown(self):
|
||||
shutil.rmtree(self.temp_dir)
|
||||
|
|
@ -1293,5 +1276,41 @@ class AugeasVhostsTest(util.ApacheTest):
|
|||
vhs = self.config.get_virtual_hosts()
|
||||
self.assertEqual([], vhs)
|
||||
|
||||
def test_choose_vhost_with_matching_wildcard(self):
|
||||
names = (
|
||||
"an.example.net", "another.example.net", "an.other.example.net")
|
||||
for name in names:
|
||||
self.assertFalse(name in self.config.choose_vhost(name).aliases)
|
||||
|
||||
def test_choose_vhost_without_matching_wildcard(self):
|
||||
mock_path = "certbot_apache.display_ops.select_vhost"
|
||||
with mock.patch(mock_path, lambda _, vhosts: vhosts[0]):
|
||||
for name in ("a.example.net", "other.example.net"):
|
||||
self.assertTrue(name in self.config.choose_vhost(name).aliases)
|
||||
|
||||
def test_choose_vhost_wildcard_not_found(self):
|
||||
mock_path = "certbot_apache.display_ops.select_vhost"
|
||||
names = (
|
||||
"abc.example.net", "not.there.tld", "aa.wildcard.tld"
|
||||
)
|
||||
with mock.patch(mock_path) as mock_select:
|
||||
mock_select.return_value = self.config.vhosts[0]
|
||||
for name in names:
|
||||
orig_cc = mock_select.call_count
|
||||
self.config.choose_vhost(name)
|
||||
self.assertEqual(mock_select.call_count - orig_cc, 1)
|
||||
|
||||
def test_choose_vhost_wildcard_found(self):
|
||||
mock_path = "certbot_apache.display_ops.select_vhost"
|
||||
names = (
|
||||
"ab.example.net", "a.wildcard.tld", "yetanother.example.net"
|
||||
)
|
||||
with mock.patch(mock_path) as mock_select:
|
||||
mock_select.return_value = self.config.vhosts[0]
|
||||
for name in names:
|
||||
self.config.choose_vhost(name)
|
||||
self.assertEqual(mock_select.call_count, 0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main() # pragma: no cover
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
"""Test certbot_apache.display_ops."""
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
import mock
|
||||
import zope.component
|
||||
|
||||
from certbot import errors
|
||||
|
||||
from certbot.display import util as display_util
|
||||
from certbot import errors
|
||||
|
||||
from certbot.tests import util as certbot_util
|
||||
|
||||
from certbot_apache import obj
|
||||
|
||||
|
|
@ -17,7 +18,6 @@ class SelectVhostTest(unittest.TestCase):
|
|||
"""Tests for certbot_apache.display_ops.select_vhost."""
|
||||
|
||||
def setUp(self):
|
||||
zope.component.provideUtility(display_util.FileDisplay(sys.stdout))
|
||||
self.base_dir = "/example_path"
|
||||
self.vhosts = util.get_vh_truth(
|
||||
self.base_dir, "debian_apache_2_4/multiple_vhosts")
|
||||
|
|
@ -27,12 +27,12 @@ class SelectVhostTest(unittest.TestCase):
|
|||
from certbot_apache.display_ops import select_vhost
|
||||
return select_vhost("example.com", vhosts)
|
||||
|
||||
@mock.patch("certbot_apache.display_ops.zope.component.getUtility")
|
||||
@certbot_util.patch_get_utility()
|
||||
def test_successful_choice(self, mock_util):
|
||||
mock_util().menu.return_value = (display_util.OK, 3)
|
||||
self.assertEqual(self.vhosts[3], self._call(self.vhosts))
|
||||
|
||||
@mock.patch("certbot_apache.display_ops.zope.component.getUtility")
|
||||
@certbot_util.patch_get_utility()
|
||||
def test_noninteractive(self, mock_util):
|
||||
mock_util().menu.side_effect = errors.MissingCommandlineFlag("no vhost default")
|
||||
try:
|
||||
|
|
@ -40,7 +40,7 @@ class SelectVhostTest(unittest.TestCase):
|
|||
except errors.MissingCommandlineFlag as e:
|
||||
self.assertTrue("vhost ambiguity" in e.message)
|
||||
|
||||
@mock.patch("certbot_apache.display_ops.zope.component.getUtility")
|
||||
@certbot_util.patch_get_utility()
|
||||
def test_more_info_cancel(self, mock_util):
|
||||
mock_util().menu.side_effect = [
|
||||
(display_util.HELP, 1),
|
||||
|
|
@ -55,7 +55,7 @@ class SelectVhostTest(unittest.TestCase):
|
|||
self.assertEqual(self._call([]), None)
|
||||
|
||||
@mock.patch("certbot_apache.display_ops.display_util")
|
||||
@mock.patch("certbot_apache.display_ops.zope.component.getUtility")
|
||||
@certbot_util.patch_get_utility()
|
||||
@mock.patch("certbot_apache.display_ops.logger")
|
||||
def test_small_display(self, mock_logger, mock_util, mock_display_util):
|
||||
mock_display_util.WIDTH = 20
|
||||
|
|
@ -64,7 +64,7 @@ class SelectVhostTest(unittest.TestCase):
|
|||
|
||||
self.assertEqual(mock_logger.debug.call_count, 1)
|
||||
|
||||
@mock.patch("certbot_apache.display_ops.zope.component.getUtility")
|
||||
@certbot_util.patch_get_utility()
|
||||
def test_multiple_names(self, mock_util):
|
||||
mock_util().menu.return_value = (display_util.OK, 5)
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,11 @@
|
|||
<VirtualHost *:80>
|
||||
ServerName wildcard.tld
|
||||
ServerAlias ?.wildcard.tld
|
||||
ServerAdmin webmaster@localhost
|
||||
DocumentRoot /var/www/html
|
||||
|
||||
ErrorLog ${APACHE_LOG_DIR}/error.log
|
||||
CustomLog ${APACHE_LOG_DIR}/access.log combined
|
||||
</VirtualHost>
|
||||
|
||||
# vim: syntax=apache ts=4 sw=4 sts=4 sr noet
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
<VirtualHost *:80>
|
||||
ServerName example.net
|
||||
ServerAlias ??.example.net *.other.example.net *another.example.net
|
||||
ServerAdmin webmaster@localhost
|
||||
DocumentRoot /var/www/html
|
||||
|
||||
ErrorLog ${APACHE_LOG_DIR}/error.log
|
||||
CustomLog ${APACHE_LOG_DIR}/access.log combined
|
||||
</VirtualHost>
|
||||
|
||||
# vim: syntax=apache ts=4 sw=4 sts=4 sr noet
|
||||
|
|
@ -13,7 +13,7 @@ from certbot.display import util as display_util
|
|||
|
||||
from certbot.plugins import common
|
||||
|
||||
from certbot.tests import test_util
|
||||
from certbot.tests import util as test_util
|
||||
|
||||
from certbot_apache import configurator
|
||||
from certbot_apache import constants
|
||||
|
|
@ -64,7 +64,8 @@ class ParserTest(ApacheTest): # pytlint: disable=too-few-public-methods
|
|||
vhost_root="debian_apache_2_4/multiple_vhosts/apache2/sites-available"):
|
||||
super(ParserTest, self).setUp(test_dir, config_root, vhost_root)
|
||||
|
||||
zope.component.provideUtility(display_util.FileDisplay(sys.stdout))
|
||||
zope.component.provideUtility(display_util.FileDisplay(sys.stdout,
|
||||
False))
|
||||
|
||||
from certbot_apache.parser import ApacheParser
|
||||
self.aug = augeas.Augeas(
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ from setuptools import setup
|
|||
from setuptools import find_packages
|
||||
|
||||
|
||||
version = '0.10.0.dev0'
|
||||
version = '0.11.0.dev0'
|
||||
|
||||
# Please update tox.ini when modifying dependency version requirements
|
||||
install_requires = [
|
||||
|
|
|
|||
261
certbot-auto
261
certbot-auto
|
|
@ -15,11 +15,15 @@ set -e # Work even if somebody does "sh thisscript.sh".
|
|||
|
||||
# Note: you can set XDG_DATA_HOME or VENV_PATH before running this script,
|
||||
# if you want to change where the virtual environment will be installed
|
||||
XDG_DATA_HOME=${XDG_DATA_HOME:-~/.local/share}
|
||||
if [ -z "$XDG_DATA_HOME" ]; then
|
||||
XDG_DATA_HOME=~/.local/share
|
||||
fi
|
||||
VENV_NAME="letsencrypt"
|
||||
VENV_PATH=${VENV_PATH:-"$XDG_DATA_HOME/$VENV_NAME"}
|
||||
if [ -z "$VENV_PATH" ]; then
|
||||
VENV_PATH="$XDG_DATA_HOME/$VENV_NAME"
|
||||
fi
|
||||
VENV_BIN="$VENV_PATH/bin"
|
||||
LE_AUTO_VERSION="0.9.3"
|
||||
LE_AUTO_VERSION="0.10.0"
|
||||
BASENAME=$(basename $0)
|
||||
USAGE="Usage: $BASENAME [OPTIONS]
|
||||
A self-updating wrapper script for the Certbot ACME client. When run, updates
|
||||
|
|
@ -58,6 +62,7 @@ for arg in "$@" ; do
|
|||
--verbose)
|
||||
VERBOSE=1;;
|
||||
-[!-]*)
|
||||
OPTIND=1
|
||||
while getopts ":hnvq" short_arg $arg; do
|
||||
case "$short_arg" in
|
||||
h)
|
||||
|
|
@ -79,43 +84,74 @@ if [ $BASENAME = "letsencrypt-auto" ]; then
|
|||
HELP=0
|
||||
fi
|
||||
|
||||
# Support for busybox and others where there is no "command",
|
||||
# but "which" instead
|
||||
if command -v command > /dev/null 2>&1 ; then
|
||||
export EXISTS="command -v"
|
||||
elif which which > /dev/null 2>&1 ; then
|
||||
export EXISTS="which"
|
||||
else
|
||||
echo "Cannot find command nor which... please install one!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# certbot-auto needs root access to bootstrap OS dependencies, and
|
||||
# certbot itself needs root access for almost all modes of operation
|
||||
# The "normal" case is that sudo is used for the steps that need root, but
|
||||
# this script *can* be run as root (not recommended), or fall back to using
|
||||
# `su`
|
||||
# `su`. Auto-detection can be overrided by explicitly setting the
|
||||
# environment variable LE_AUTO_SUDO to 'sudo', 'sudo_su' or '' as used below.
|
||||
|
||||
# Because the parameters in `su -c` has to be a string,
|
||||
# we need to properly escape it.
|
||||
su_sudo() {
|
||||
args=""
|
||||
# This `while` loop iterates over all parameters given to this function.
|
||||
# For each parameter, all `'` will be replace by `'"'"'`, and the escaped string
|
||||
# will be wrapped in a pair of `'`, then appended to `$args` string
|
||||
# For example, `echo "It's only 1\$\!"` will be escaped to:
|
||||
# 'echo' 'It'"'"'s only 1$!'
|
||||
# │ │└┼┘│
|
||||
# │ │ │ └── `'s only 1$!'` the literal string
|
||||
# │ │ └── `\"'\"` is a single quote (as a string)
|
||||
# │ └── `'It'`, to be concatenated with the strings following it
|
||||
# └── `echo` wrapped in a pair of `'`, it's totally fine for the shell command itself
|
||||
while [ $# -ne 0 ]; do
|
||||
args="$args'$(printf "%s" "$1" | sed -e "s/'/'\"'\"'/g")' "
|
||||
shift
|
||||
done
|
||||
su root -c "$args"
|
||||
}
|
||||
|
||||
SUDO_ENV=""
|
||||
export CERTBOT_AUTO="$0"
|
||||
if test "`id -u`" -ne "0" ; then
|
||||
if command -v sudo 1>/dev/null 2>&1; then
|
||||
SUDO=sudo
|
||||
SUDO_ENV="CERTBOT_AUTO=$0"
|
||||
else
|
||||
echo \"sudo\" is not available, will use \"su\" for installation steps...
|
||||
# Because the parameters in `su -c` has to be a string,
|
||||
# we need properly escape it
|
||||
su_sudo() {
|
||||
args=""
|
||||
# This `while` loop iterates over all parameters given to this function.
|
||||
# For each parameter, all `'` will be replace by `'"'"'`, and the escaped string
|
||||
# will be wrapped in a pair of `'`, then appended to `$args` string
|
||||
# For example, `echo "It's only 1\$\!"` will be escaped to:
|
||||
# 'echo' 'It'"'"'s only 1$!'
|
||||
# │ │└┼┘│
|
||||
# │ │ │ └── `'s only 1$!'` the literal string
|
||||
# │ │ └── `\"'\"` is a single quote (as a string)
|
||||
# │ └── `'It'`, to be concatenated with the strings following it
|
||||
# └── `echo` wrapped in a pair of `'`, it's totally fine for the shell command itself
|
||||
while [ $# -ne 0 ]; do
|
||||
args="$args'$(printf "%s" "$1" | sed -e "s/'/'\"'\"'/g")' "
|
||||
shift
|
||||
done
|
||||
su root -c "$args"
|
||||
}
|
||||
SUDO=su_sudo
|
||||
fi
|
||||
if [ -n "${LE_AUTO_SUDO+x}" ]; then
|
||||
case "$LE_AUTO_SUDO" in
|
||||
su_sudo|su)
|
||||
SUDO=su_sudo
|
||||
;;
|
||||
sudo)
|
||||
SUDO=sudo
|
||||
SUDO_ENV="CERTBOT_AUTO=$0"
|
||||
;;
|
||||
'') ;; # Nothing to do for plain root method.
|
||||
*)
|
||||
echo "Error: unknown root authorization mechanism '$LE_AUTO_SUDO'."
|
||||
exit 1
|
||||
esac
|
||||
echo "Using preset root authorization mechanism '$LE_AUTO_SUDO'."
|
||||
else
|
||||
SUDO=
|
||||
if test "`id -u`" -ne "0" ; then
|
||||
if $EXISTS sudo 1>/dev/null 2>&1; then
|
||||
SUDO=sudo
|
||||
SUDO_ENV="CERTBOT_AUTO=$0"
|
||||
else
|
||||
echo \"sudo\" is not available, will use \"su\" for installation steps...
|
||||
SUDO=su_sudo
|
||||
fi
|
||||
else
|
||||
SUDO=
|
||||
fi
|
||||
fi
|
||||
|
||||
ExperimentalBootstrap() {
|
||||
|
|
@ -136,7 +172,7 @@ ExperimentalBootstrap() {
|
|||
DeterminePythonVersion() {
|
||||
for LE_PYTHON in "$LE_PYTHON" python2.7 python27 python2 python; do
|
||||
# Break (while keeping the LE_PYTHON value) if found.
|
||||
command -v "$LE_PYTHON" > /dev/null && break
|
||||
$EXISTS "$LE_PYTHON" > /dev/null && break
|
||||
done
|
||||
if [ "$?" != "0" ]; then
|
||||
echo "Cannot find any Pythons; please install one!"
|
||||
|
|
@ -177,19 +213,22 @@ BootstrapDebCommon() {
|
|||
# distro version (#346)
|
||||
|
||||
virtualenv=
|
||||
if apt-cache show virtualenv > /dev/null 2>&1 && ! apt-cache --quiet=0 show virtualenv 2>&1 | grep -q 'No packages found'; then
|
||||
virtualenv="virtualenv"
|
||||
# virtual env is known to apt and is installable
|
||||
if apt-cache show virtualenv > /dev/null 2>&1 ; then
|
||||
if ! LC_ALL=C apt-cache --quiet=0 show virtualenv 2>&1 | grep -q 'No packages found'; then
|
||||
virtualenv="virtualenv"
|
||||
fi
|
||||
fi
|
||||
|
||||
if apt-cache show python-virtualenv > /dev/null 2>&1; then
|
||||
virtualenv="$virtualenv python-virtualenv"
|
||||
virtualenv="$virtualenv python-virtualenv"
|
||||
fi
|
||||
|
||||
augeas_pkg="libaugeas0 augeas-lenses"
|
||||
AUGVERSION=`apt-cache show --no-all-versions libaugeas0 | grep ^Version: | cut -d" " -f2`
|
||||
AUGVERSION=`LC_ALL=C apt-cache show --no-all-versions libaugeas0 | grep ^Version: | cut -d" " -f2`
|
||||
|
||||
if [ "$ASSUME_YES" = 1 ]; then
|
||||
YES_FLAG="-y"
|
||||
YES_FLAG="-y"
|
||||
fi
|
||||
|
||||
AddBackportRepo() {
|
||||
|
|
@ -248,15 +287,15 @@ BootstrapDebCommon() {
|
|||
python-dev \
|
||||
$virtualenv \
|
||||
gcc \
|
||||
dialog \
|
||||
$augeas_pkg \
|
||||
libssl-dev \
|
||||
openssl \
|
||||
libffi-dev \
|
||||
ca-certificates \
|
||||
|
||||
|
||||
|
||||
if ! command -v virtualenv > /dev/null ; then
|
||||
if ! $EXISTS virtualenv > /dev/null ; then
|
||||
echo Failed to install a working \"virtualenv\" command, exiting
|
||||
exit 1
|
||||
fi
|
||||
|
|
@ -307,7 +346,6 @@ BootstrapRpmCommon() {
|
|||
|
||||
pkgs="
|
||||
gcc
|
||||
dialog
|
||||
augeas-libs
|
||||
openssl
|
||||
openssl-devel
|
||||
|
|
@ -361,7 +399,6 @@ BootstrapSuseCommon() {
|
|||
python-devel \
|
||||
python-virtualenv \
|
||||
gcc \
|
||||
dialog \
|
||||
augeas-lenses \
|
||||
libopenssl-devel \
|
||||
libffi-devel \
|
||||
|
|
@ -380,7 +417,6 @@ BootstrapArchCommon() {
|
|||
python2
|
||||
python-virtualenv
|
||||
gcc
|
||||
dialog
|
||||
augeas
|
||||
openssl
|
||||
libffi
|
||||
|
|
@ -404,22 +440,26 @@ BootstrapGentooCommon() {
|
|||
PACKAGES="
|
||||
dev-lang/python:2.7
|
||||
dev-python/virtualenv
|
||||
dev-util/dialog
|
||||
app-admin/augeas
|
||||
dev-libs/openssl
|
||||
dev-libs/libffi
|
||||
app-misc/ca-certificates
|
||||
virtual/pkgconfig"
|
||||
|
||||
ASK_OPTION="--ask"
|
||||
if [ "$ASSUME_YES" = 1 ]; then
|
||||
ASK_OPTION=""
|
||||
fi
|
||||
|
||||
case "$PACKAGE_MANAGER" in
|
||||
(paludis)
|
||||
$SUDO cave resolve --preserve-world --keep-targets if-possible $PACKAGES -x
|
||||
;;
|
||||
(pkgcore)
|
||||
$SUDO pmerge --noreplace --oneshot $PACKAGES
|
||||
$SUDO pmerge --noreplace --oneshot $ASK_OPTION $PACKAGES
|
||||
;;
|
||||
(portage|*)
|
||||
$SUDO emerge --noreplace --oneshot $PACKAGES
|
||||
$SUDO emerge --noreplace --oneshot $ASK_OPTION $PACKAGES
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
|
@ -449,7 +489,6 @@ BootstrapMac() {
|
|||
fi
|
||||
|
||||
$pkgcmd augeas
|
||||
$pkgcmd dialog
|
||||
if [ "$(which python)" = "/System/Library/Frameworks/Python.framework/Versions/2.7/bin/python" \
|
||||
-o "$(which python)" = "/usr/bin/python" ]; then
|
||||
# We want to avoid using the system Python because it requires root to use pip.
|
||||
|
|
@ -458,7 +497,7 @@ BootstrapMac() {
|
|||
$pkgcmd python
|
||||
fi
|
||||
|
||||
# Workaround for _dlopen not finding augeas on OS X
|
||||
# Workaround for _dlopen not finding augeas on macOS
|
||||
if [ "$pkgman" = "port" ] && ! [ -e "/usr/local/lib/libaugeas.dylib" ] && [ -e "/opt/local/lib/libaugeas.dylib" ]; then
|
||||
echo "Applying augeas workaround"
|
||||
$SUDO mkdir -p /usr/local/lib/
|
||||
|
|
@ -496,8 +535,8 @@ BootstrapMageiaCommon() {
|
|||
if ! $SUDO urpmi --force \
|
||||
git \
|
||||
gcc \
|
||||
cdialog \
|
||||
python-augeas \
|
||||
openssl \
|
||||
libopenssl-devel \
|
||||
libffi-devel \
|
||||
rootcerts
|
||||
|
|
@ -541,7 +580,7 @@ Bootstrap() {
|
|||
elif uname | grep -iq FreeBSD ; then
|
||||
ExperimentalBootstrap "FreeBSD" BootstrapFreeBsd
|
||||
elif uname | grep -iq Darwin ; then
|
||||
ExperimentalBootstrap "Mac OS X" BootstrapMac
|
||||
ExperimentalBootstrap "macOS" BootstrapMac
|
||||
elif [ -f /etc/issue ] && grep -iq "Amazon Linux" /etc/issue ; then
|
||||
ExperimentalBootstrap "Amazon Linux" BootstrapRpmCommon
|
||||
elif [ -f /etc/product ] && grep -q "Joyent Instance" /etc/product ; then
|
||||
|
|
@ -557,7 +596,7 @@ Bootstrap() {
|
|||
}
|
||||
|
||||
TempDir() {
|
||||
mktemp -d 2>/dev/null || mktemp -d -t 'le' # Linux || OS X
|
||||
mktemp -d 2>/dev/null || mktemp -d -t 'le' # Linux || macOS
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -594,6 +633,11 @@ if [ "$1" = "--le-auto-phase2" ]; then
|
|||
# `pip install --no-cache-dir -e acme -e . -e certbot-apache -e certbot-nginx`,
|
||||
# and then use `hashin` or a more secure method to gather the hashes.
|
||||
|
||||
# Hashin example:
|
||||
# pip install hashin
|
||||
# hashin -r letsencrypt-auto-requirements.txt cryptography==1.5.2
|
||||
# sets the new certbot-auto pinned version of cryptography to 1.5.2
|
||||
|
||||
argparse==1.4.0 \
|
||||
--hash=sha256:c31647edb69fd3d465a847ea3157d37bed1f95f19760b11a47aa91c04b666314 \
|
||||
--hash=sha256:62b089a55be1d8949cd2bc7e0df0bddb9e028faefc8c32038cc84862aefdd6e4
|
||||
|
|
@ -601,7 +645,8 @@ argparse==1.4.0 \
|
|||
# This comes before cffi because cffi will otherwise install an unchecked
|
||||
# version via setup_requires.
|
||||
pycparser==2.14 \
|
||||
--hash=sha256:7959b4a74abdc27b312fed1c21e6caf9309ce0b29ea86b591fd2e99ecdf27f73
|
||||
--hash=sha256:7959b4a74abdc27b312fed1c21e6caf9309ce0b29ea86b591fd2e99ecdf27f73 \
|
||||
--no-binary pycparser
|
||||
|
||||
cffi==1.4.2 \
|
||||
--hash=sha256:53c1c9ddb30431513eb7f3cdef0a3e06b0f1252188aaa7744af0f5a4cd45dbaf \
|
||||
|
|
@ -624,29 +669,29 @@ ConfigArgParse==0.10.0 \
|
|||
--hash=sha256:3b50a83dd58149dfcee98cb6565265d10b53e9c0a2bca7eeef7fb5f5524890a7
|
||||
configobj==5.0.6 \
|
||||
--hash=sha256:a2f5650770e1c87fb335af19a9b7eb73fc05ccf22144eb68db7d00cd2bcb0902
|
||||
cryptography==1.3.4 \
|
||||
--hash=sha256:bede00edd11a2a62c8c98c271cc103fa3a3d72acf64f6e5e4eaf251128897b17 \
|
||||
--hash=sha256:53b39e687b744bb548a98f40736cc529d9f60959b4e6cc551322cf9505d35eb3 \
|
||||
--hash=sha256:474b73ad1139b4e423e46bbd818efd0d5c0df1c65d9f7c957d64c9215d77afde \
|
||||
--hash=sha256:aaddf9592d5b99e32dd518bb4a25b147c124f9d6b4ad64b94f01b15d1666b8c8 \
|
||||
--hash=sha256:6dcad2f407db8c3cd6ecd78361439c449a4f94786b46c54507e7e68f51e1709d \
|
||||
--hash=sha256:475c153fc622e656f1f10a9c9941d0ac7ab18df7c38d35d563a437c1c0e34f24 \
|
||||
--hash=sha256:86dd61df581cba04e89e45081efbc531faff1c9d99c77b1ce97f87216c356353 \
|
||||
--hash=sha256:75cc697e4ef5fdd0102ca749114c6370dbd11db0c9132a18834858c2566247e3 \
|
||||
--hash=sha256:ea03ad5b9df6d79fc9fc1ab23729e01e1c920d2974c5e3c634ccf45a5c378452 \
|
||||
--hash=sha256:c8872b8fe4f3416d6338ab99612f49ab314f7856cb43bffab2a32d28a6267be8 \
|
||||
--hash=sha256:468fc6e16eaec6ceaa6bc341273e6e9912d01b42b740f8cf896ace7fcd6a321d \
|
||||
--hash=sha256:d6fea3c6502735011c5d61a62aef1c1d770fc6a2def45d9e6c0d94c9651e3317 \
|
||||
--hash=sha256:3cf95f179f4bead3d5649b91860ef4cf60ad4244209190fc405908272576d961 \
|
||||
--hash=sha256:141f77e60a5b9158309b2b60288c7f81d37faa15c22a69b94c190ceefaaa6236 \
|
||||
--hash=sha256:87b7a1fe703c6424451f3372d1879dae91c7fe5e13375441a72833db76fee30e \
|
||||
--hash=sha256:f5ee3cb0cf1a6550bf483ccffa6608db267a377b45f7e3a8201a86d1d8feb19f \
|
||||
--hash=sha256:4e097286651ea318300af3251375d48b71b8228481c56cd617ddd4459a1ff261 \
|
||||
--hash=sha256:1e3d3ae3f22f22d50d340f47f25227511326f3f1396c6d2446a5b45b516c4313 \
|
||||
--hash=sha256:6a057941cb64d79834ea3cf99093fcc4787c2a5d44f686c4f297361ddc419bcd \
|
||||
--hash=sha256:68b3d5390b92559ddd3353c73ab2dfcff758f9c4ec4f5d5226ccede0e5d779f4 \
|
||||
--hash=sha256:545dc003b4b6081f9c3e452da15d819b04b696f49484aff64c0a2aedf766bef8 \
|
||||
--hash=sha256:423ff890c01be7c70dbfeaa967eeef5146f1a43a5f810ffdc07b178e48a105a9
|
||||
cryptography==1.5.3 \
|
||||
--hash=sha256:e514d92086246b53ae9b048df652cf3036b462e50a6ce9fac6b6253502679991 \
|
||||
--hash=sha256:10ee414f4b5af403a0d8f20dfa80f7dad1fc7ae5452ec5af03712d5b6e78c664 \
|
||||
--hash=sha256:7234456d1f4345a144ed07af2416c7c0659d4bb599dd1a963103dc8c183b370e \
|
||||
--hash=sha256:d3b9587406f94642bd70b3d666b813f446e95f84220c9e416ad94cbfb6be2eaa \
|
||||
--hash=sha256:b15fc6b59f1474eef62207c85888afada8acc47fae8198ba2b0197d54538961a \
|
||||
--hash=sha256:3b62d65d342704fc07ed171598db2a2775bdf587b1b6abd2cba2261bfe3ccde3 \
|
||||
--hash=sha256:059343022ec904c867a13bc55d2573e36c8cfb2c250e30d8a2e9825f253b07ba \
|
||||
--hash=sha256:c7897cf13bc8b4ee0215d83cbd51766d87c06b277fcca1f9108595508e5bcfb4 \
|
||||
--hash=sha256:9b69e983e5bf83039ddd52e52a28c7faedb2b22bdfb5876377b95aac7d3be63e \
|
||||
--hash=sha256:61e40905c426d02b3fae38088dc66ce4ef84830f7eb223dec6b3ac3ccdc676fb \
|
||||
--hash=sha256:00783a32bcd91a12177230d35bfcf70a2333ade4a6b607fac94a633a7971c671 \
|
||||
--hash=sha256:d11973f49b648cde1ea1a30e496d7557dbfeccd08b3cd9ba58d286a9c274ff8e \
|
||||
--hash=sha256:f24bedf28b81932ba6063aec9a826669f5237ea3b755efe04d98b072faa053a5 \
|
||||
--hash=sha256:3ab5725367239e3deb9b92e917aa965af3fef008f25b96a3000821869e208181 \
|
||||
--hash=sha256:8a53209de822e22b5f73bf4b99e68ac4ccc91051fd6751c8252982983e86a77d \
|
||||
--hash=sha256:5a07439d4b1e4197ac202b7eea45e26a6fd65757652dc50f1a63367f711df933 \
|
||||
--hash=sha256:26b1c4b40aec7b0074bceabe6e06565aa28176eca7323a31df66ebf89fe916d3 \
|
||||
--hash=sha256:eaa4a7b5a6682adcf8d6ebb2a08a008802657643655bb527c95c8a3860253d8e \
|
||||
--hash=sha256:8156927dcf8da274ff205ad0612f75c380df45385bacf98531a5b3348c88d135 \
|
||||
--hash=sha256:61ec0d792749d0e91e84b1d58b6dfd204806b10b5811f846c2ceca0de028c53a \
|
||||
--hash=sha256:26330c88041569ca621cc42274d0ea2667a48b6deab41467272c3aba0b6e8f07 \
|
||||
--hash=sha256:cf82ddac919b587f5e44247579b433224cc2e03332d2ea4d89aa70d7e6b64ae5
|
||||
enum34==1.1.2 \
|
||||
--hash=sha256:2475d7fcddf5951e92ff546972758802de5260bf409319a9f1934e6bbc8b1dc7 \
|
||||
--hash=sha256:35907defb0f992b75ab7788f65fedc1cf20ffa22688e0e6f6f12afc06b3ea501
|
||||
|
|
@ -662,8 +707,6 @@ ipaddress==1.0.16 \
|
|||
linecache2==1.0.0 \
|
||||
--hash=sha256:e78be9c0a0dfcbac712fe04fbf92b96cddae80b1b842f24248214c8496f006ef \
|
||||
--hash=sha256:4b26ff4e7110db76eeb6f5a7b64a82623839d595c2038eeda662f2a2db78e97c
|
||||
ndg-httpsclient==0.4.0 \
|
||||
--hash=sha256:e8c155fdebd9c4bcb0810b4ed01ae1987554b1ee034dd7532d7b8fdae38a6274
|
||||
ordereddict==1.1 \
|
||||
--hash=sha256:1c35b4ac206cef2d24816c89f89cf289dd3d38cf7c449bb3fab7bf6d43f01b1f
|
||||
parsedatetime==2.1 \
|
||||
|
|
@ -684,9 +727,9 @@ pyasn1==0.1.9 \
|
|||
--hash=sha256:5191ff6b9126d2c039dd87f8ff025bed274baf07fa78afa46f556b1ad7265d6e \
|
||||
--hash=sha256:8323e03637b2d072cc7041300bac6ec448c3c28950ab40376036788e9a1af629 \
|
||||
--hash=sha256:853cacd96d1f701ddd67aa03ecc05f51890135b7262e922710112f12a2ed2a7f
|
||||
pyopenssl==16.0.0 \
|
||||
--hash=sha256:5add70cf00273bf957ca31fdb0df9b0ae4639e081897d5f86a0ae1f104901230 \
|
||||
--hash=sha256:363d10ee43d062285facf4e465f4f5163f9f702f9134f0a5896f134cbb92d17d
|
||||
pyOpenSSL==16.2.0 \
|
||||
--hash=sha256:26ca380ddf272f7556e48064bbcd5bd71f83dfc144f3583501c7ddbd9434ee17 \
|
||||
--hash=sha256:7779a3bbb74e79db234af6a08775568c6769b5821faecf6e2f4143edb227516e
|
||||
pyparsing==2.1.8 \
|
||||
--hash=sha256:2f0f5ceb14eccd5aef809d6382e87df22ca1da583c79f6db01675ce7d7f49c18 \
|
||||
--hash=sha256:03a4869b9f3493807ee1f1cb405e6d576a1a2ca4d81a982677c0c1ad6177c56b \
|
||||
|
|
@ -701,9 +744,6 @@ pyRFC3339==1.0 \
|
|||
--hash=sha256:8dfbc6c458b8daba1c0f3620a8c78008b323a268b27b7359e92a4ae41325f535
|
||||
python-augeas==0.5.0 \
|
||||
--hash=sha256:67d59d66cdba8d624e0389b87b2a83a176f21f16a87553b50f5703b23f29bac2
|
||||
python2-pythondialog==3.3.0 \
|
||||
--hash=sha256:04e93f24995c43dd90f338d5d865ca72ce3fb5a5358d4daa4965571db35fc3ec \
|
||||
--hash=sha256:3e6f593fead98f8a526bc3e306933533236e33729f552f52896ea504f55313fa
|
||||
pytz==2015.7 \
|
||||
--hash=sha256:3abe6a6d3fc2fbbe4c60144211f45da2edbe3182a6f6511af6bbba0598b1f992 \
|
||||
--hash=sha256:939ef9c1e1224d980405689a97ffcf7828c56d1517b31d73464356c1f2b7769e \
|
||||
|
|
@ -718,9 +758,9 @@ pytz==2015.7 \
|
|||
--hash=sha256:fbd26746772c24cb93c8b97cbdad5cb9e46c86bbdb1b9d8a743ee00e2fb1fc5d \
|
||||
--hash=sha256:99266ef30a37e43932deec2b7ca73e83c8dbc3b9ff703ec73eca6b1dae6befea \
|
||||
--hash=sha256:8b6ce1c993909783bc96e0b4f34ea223bff7a4df2c90bdb9c4e0f1ac928689e3
|
||||
requests==2.9.1 \
|
||||
--hash=sha256:113fbba5531a9e34945b7d36b33a084e8ba5d0664b703c81a7c572d91919a5b8 \
|
||||
--hash=sha256:c577815dd00f1394203fc44eb979724b098f88264a9ef898ee45b8e5e9cf587f
|
||||
requests==2.12.1 \
|
||||
--hash=sha256:3f3f27a9d0f9092935efc78054ef324eb9f8166718270aefe036dfa1e4f68e1e \
|
||||
--hash=sha256:2109ecea94df90980be040490ff1d879971b024861539abb00054062388b612e
|
||||
six==1.10.0 \
|
||||
--hash=sha256:0ff78c403d9bccf5a425a6d31a12aa6b47f1c21ca4dc2573a7e2f32a97335eb1 \
|
||||
--hash=sha256:105f8d68616f8248e24bf0e9372ef04d3cc10104f1980f54d57b2ce73a5ad56a
|
||||
|
|
@ -761,18 +801,18 @@ letsencrypt==0.7.0 \
|
|||
|
||||
# THE LINES BELOW ARE EDITED BY THE RELEASE SCRIPT; ADD ALL DEPENDENCIES ABOVE.
|
||||
|
||||
acme==0.9.3 \
|
||||
--hash=sha256:d18ce17a75ad24d27981dfaef0524aa905eab757b267e027162b56a8967ab8fb \
|
||||
--hash=sha256:a6eff1f955eb2e4316abd9aa2fedb6d9345e6b5b8a2d64ea0ad35e05d6124099
|
||||
certbot==0.9.3 \
|
||||
--hash=sha256:a87ef4c53c018df4e52ee2f2e906ad16bbb37789f29e6f284c495a2eb4d9b243 \
|
||||
--hash=sha256:68149cb8392b29f5d5246e7226d25f913f2b10482bf3bc7368e8c8821d25f3b0
|
||||
certbot-apache==0.9.3 \
|
||||
--hash=sha256:f379b1053e10709692654d7a6fcea9eaed19b66c49a753b61e31bd06a04b0aac \
|
||||
--hash=sha256:a5d98cf972072de08f984db4e6a7f20269f3f023c43f6d4e781fe43be7c10086
|
||||
certbot-nginx==0.9.3 \
|
||||
--hash=sha256:3c26f18f0b57550f069263bd9b2984ef33eab6693e7796611c1b2cc16574069c \
|
||||
--hash=sha256:7337a2e90e0b28a1ab09e31d9fb81c6d78e6453500c824c0f18bab5d31b63058
|
||||
acme==0.10.0 \
|
||||
--hash=sha256:df4299a9881d94185a1578ed97334430a90f761ce815edd300860ca47d0538f1 \
|
||||
--hash=sha256:ddebdf1fe139c8fedbcf633955ec867496d2f7d2d2e9879d538437a69ab47876
|
||||
certbot==0.10.0 \
|
||||
--hash=sha256:fb1bfa3d54ce9366758e374f7ed99667ce20484224934d3e8e57839fcf784bc5 \
|
||||
--hash=sha256:dd64ed8fb3cc3b053f05e779b934433445918668c49bcdbb2c816062815e1661
|
||||
certbot-apache==0.10.0 \
|
||||
--hash=sha256:909d59c53507093f838f7336f75d7d78563a35b16afdf6c30f45c9f47bf069da \
|
||||
--hash=sha256:6f110dae227dd0fea9572fa12dd60b041e391f5d2028cc2e1fedd2a9a0d2bc88
|
||||
certbot-nginx==0.10.0 \
|
||||
--hash=sha256:4f33a230d420cbd0431e7b707fb9a1732bfd18d3c6056019591bd7c3a13abe92 \
|
||||
--hash=sha256:c12ffd05207b0be3c765b3d3e2927e0b2cc2b7de20654b19d154a0d789e7c1d5
|
||||
|
||||
UNLIKELY_EOF
|
||||
# -------------------------------------------------------------------------
|
||||
|
|
@ -940,7 +980,28 @@ UNLIKELY_EOF
|
|||
# Report error. (Otherwise, be quiet.)
|
||||
echo "Had a problem while installing Python packages."
|
||||
if [ "$VERBOSE" != 1 ]; then
|
||||
echo
|
||||
echo "pip prints the following errors: "
|
||||
echo "====================================================="
|
||||
echo "$PIP_OUT"
|
||||
echo "====================================================="
|
||||
echo
|
||||
echo "Certbot has problem setting up the virtual environment."
|
||||
|
||||
if `echo $PIP_OUT | grep -q Killed` || `echo $PIP_OUT | grep -q "allocate memory"` ; then
|
||||
echo
|
||||
echo "Based on your pip output, the problem can likely be fixed by "
|
||||
echo "increasing the available memory."
|
||||
else
|
||||
echo
|
||||
echo "We were not be able to guess the right solution from your pip "
|
||||
echo "output."
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "Consult https://certbot.eff.org/docs/install.html#problems-with-python-virtual-environment"
|
||||
echo "for possible solutions."
|
||||
echo "You may also find some support resources at https://certbot.eff.org/support/ ."
|
||||
fi
|
||||
rm -rf "$VENV_PATH"
|
||||
exit 1
|
||||
|
|
@ -1132,7 +1193,7 @@ UNLIKELY_EOF
|
|||
# TODO: Deal with quotes in pathnames.
|
||||
echo "Replacing certbot-auto..."
|
||||
# Clone permissions with cp. chmod and chown don't have a --reference
|
||||
# option on OS X or BSD, and stat -c on Linux is stat -f on OS X and BSD:
|
||||
# option on macOS or BSD, and stat -c on Linux is stat -f on macOS and BSD:
|
||||
$SUDO cp -p "$0" "$TEMP_DIR/letsencrypt-auto.permission-clone"
|
||||
$SUDO cp "$TEMP_DIR/letsencrypt-auto" "$TEMP_DIR/letsencrypt-auto.permission-clone"
|
||||
# Using mv rather than cp leaves the old file descriptor pointing to the
|
||||
|
|
|
|||
|
|
@ -38,8 +38,7 @@ RUN virtualenv --no-site-packages -p python2 /opt/certbot/venv && \
|
|||
-e /opt/certbot/src \
|
||||
-e /opt/certbot/src/certbot-apache \
|
||||
-e /opt/certbot/src/certbot-nginx \
|
||||
-e /opt/certbot/src/certbot-compatibility-test \
|
||||
-e /opt/certbot/src[dev,docs]
|
||||
-e /opt/certbot/src/certbot-compatibility-test
|
||||
|
||||
# install in editable mode (-e) to save space: it's not possible to
|
||||
# "rm -rf /opt/certbot/src" (it's stays in the underlaying image);
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ from setuptools import setup
|
|||
from setuptools import find_packages
|
||||
|
||||
|
||||
version = '0.10.0.dev0'
|
||||
version = '0.11.0.dev0'
|
||||
|
||||
install_requires = [
|
||||
'certbot',
|
||||
|
|
|
|||
|
|
@ -29,6 +29,36 @@ from certbot_nginx import parser
|
|||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
REDIRECT_BLOCK = [[
|
||||
['\n ', 'if', ' ', '($scheme != "https") '],
|
||||
[['\n ', 'return', ' ', '301 https://$host$request_uri'],
|
||||
'\n ']
|
||||
], ['\n']]
|
||||
|
||||
TEST_REDIRECT_BLOCK = [
|
||||
[
|
||||
['if', '($scheme != "https")'],
|
||||
[
|
||||
['return', '301 https://$host$request_uri']
|
||||
]
|
||||
],
|
||||
['#', ' managed by Certbot']
|
||||
]
|
||||
|
||||
REDIRECT_COMMENT_BLOCK = [
|
||||
['\n ', '#', ' Redirect non-https traffic to https'],
|
||||
['\n ', '#', ' if ($scheme != "https") {'],
|
||||
['\n ', '#', " return 301 https://$host$request_uri;"],
|
||||
['\n ', '#', " } # managed by Certbot"],
|
||||
['\n']
|
||||
]
|
||||
|
||||
TEST_REDIRECT_COMMENT_BLOCK = [
|
||||
['#', ' Redirect non-https traffic to https'],
|
||||
['#', ' if ($scheme != "https") {'],
|
||||
['#', " return 301 https://$host$request_uri;"],
|
||||
['#', " } # managed by Certbot"],
|
||||
]
|
||||
|
||||
@zope.interface.implementer(interfaces.IAuthenticator, interfaces.IInstaller)
|
||||
@zope.interface.provider(interfaces.IPluginFactory)
|
||||
|
|
@ -161,11 +191,9 @@ class NginxConfigurator(common.Plugin):
|
|||
vhost.filep, vhost.names)
|
||||
except errors.MisconfigurationError as error:
|
||||
logger.debug(error)
|
||||
logger.warning(
|
||||
"Cannot find a cert or key directive in %s for %s. "
|
||||
"VirtualHost was not modified.", vhost.filep, vhost.names)
|
||||
# Presumably break here so that the virtualhost is not modified
|
||||
return False
|
||||
raise errors.PluginError("Cannot find a cert or key directive in {0} for {1}. "
|
||||
"VirtualHost was not modified.".format(vhost.filep, vhost.names))
|
||||
|
||||
self.save_notes += ("Changed vhost at %s with addresses of %s\n" %
|
||||
(vhost.filep,
|
||||
|
|
@ -405,7 +433,8 @@ class NginxConfigurator(common.Plugin):
|
|||
cert = acme_crypto_util.gen_ss_cert(key, domains=[socket.gethostname()])
|
||||
cert_pem = OpenSSL.crypto.dump_certificate(
|
||||
OpenSSL.crypto.FILETYPE_PEM, cert)
|
||||
cert_file, cert_path = util.unique_file(os.path.join(tmp_dir, "cert.pem"))
|
||||
cert_file, cert_path = util.unique_file(
|
||||
os.path.join(tmp_dir, "cert.pem"), mode="wb")
|
||||
with cert_file:
|
||||
cert_file.write(cert_pem)
|
||||
return cert_path, le_key.file
|
||||
|
|
@ -443,18 +472,6 @@ class NginxConfigurator(common.Plugin):
|
|||
self.parser.add_server_directives(
|
||||
vhost, ssl_block, replace=False)
|
||||
|
||||
def get_all_certs_keys(self):
|
||||
"""Find all existing keys, certs from configuration.
|
||||
|
||||
:returns: list of tuples with form [(cert, key, path)]
|
||||
cert - str path to certificate file
|
||||
key - str path to associated key file
|
||||
path - File path to configuration file.
|
||||
:rtype: set
|
||||
|
||||
"""
|
||||
return self.parser.get_all_certs_keys()
|
||||
|
||||
##################################
|
||||
# enhancement methods (IInstaller)
|
||||
##################################
|
||||
|
|
@ -482,6 +499,23 @@ class NginxConfigurator(common.Plugin):
|
|||
logger.warning("Failed %s for %s", enhancement, domain)
|
||||
raise
|
||||
|
||||
def _has_certbot_redirect(self, vhost):
|
||||
return vhost.contains_list(TEST_REDIRECT_BLOCK)
|
||||
|
||||
def _has_certbot_redirect_comment(self, vhost):
|
||||
return vhost.contains_list(TEST_REDIRECT_COMMENT_BLOCK)
|
||||
|
||||
def _add_redirect_block(self, vhost, active=True):
|
||||
"""Add redirect directive to vhost
|
||||
"""
|
||||
if active:
|
||||
redirect_block = REDIRECT_BLOCK
|
||||
else:
|
||||
redirect_block = REDIRECT_COMMENT_BLOCK
|
||||
|
||||
self.parser.add_server_directives(
|
||||
vhost, redirect_block, replace=False)
|
||||
|
||||
def _enable_redirect(self, domain, unused_options):
|
||||
"""Redirect all equivalent HTTP traffic to ssl_vhost.
|
||||
|
||||
|
|
@ -504,17 +538,20 @@ class NginxConfigurator(common.Plugin):
|
|||
logger.info("No matching insecure server blocks listening on port %s found.",
|
||||
self.DEFAULT_LISTEN_PORT)
|
||||
else:
|
||||
# Redirect plaintextish host to https
|
||||
redirect_block = [[
|
||||
['\n ', 'if', ' ', '($scheme != "https") '],
|
||||
[['\n ', 'return', ' ', '301 https://$host$request_uri'],
|
||||
'\n ']
|
||||
], ['\n']]
|
||||
|
||||
self.parser.add_server_directives(
|
||||
vhost, redirect_block, replace=False)
|
||||
logger.info("Redirecting all traffic on port %s to ssl in %s",
|
||||
self.DEFAULT_LISTEN_PORT, vhost.filep)
|
||||
if self._has_certbot_redirect(vhost):
|
||||
logger.info("Traffic on port %s already redirecting to ssl in %s",
|
||||
self.DEFAULT_LISTEN_PORT, vhost.filep)
|
||||
elif vhost.has_redirect():
|
||||
if not self._has_certbot_redirect_comment(vhost):
|
||||
self._add_redirect_block(vhost, active=False)
|
||||
logger.info("The appropriate server block is already redirecting "
|
||||
"traffic. To enable redirect anyway, uncomment the "
|
||||
"redirect lines in %s.", vhost.filep)
|
||||
else:
|
||||
# Redirect plaintextish host to https
|
||||
self._add_redirect_block(vhost, active=True)
|
||||
logger.info("Redirecting all traffic on port %s to ssl in %s",
|
||||
self.DEFAULT_LISTEN_PORT, vhost.filep)
|
||||
|
||||
def _enable_ocsp_stapling(self, domain, chain_path):
|
||||
"""Include OCSP response in TLS handshake
|
||||
|
|
@ -671,14 +708,16 @@ class NginxConfigurator(common.Plugin):
|
|||
"""
|
||||
save_files = set(self.parser.parsed.keys())
|
||||
|
||||
try:
|
||||
try: # TODO: make a common base for Apache and Nginx plugins
|
||||
# Create Checkpoint
|
||||
if temporary:
|
||||
self.reverter.add_to_temp_checkpoint(
|
||||
save_files, self.save_notes)
|
||||
# how many comments does it take
|
||||
else:
|
||||
self.reverter.add_to_checkpoint(save_files,
|
||||
self.save_notes)
|
||||
# to confuse a linter?
|
||||
except errors.ReverterError as err:
|
||||
raise errors.PluginError(str(err))
|
||||
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ class RawNginxParser(object):
|
|||
modifier = Literal("=") | Literal("~*") | Literal("~") | Literal("^~")
|
||||
|
||||
# rules
|
||||
comment = space + Literal('#') + restOfLine()
|
||||
comment = space + Literal('#') + restOfLine
|
||||
|
||||
assignment = space + key + Optional(space + value, default=None) + semicolon
|
||||
location_statement = space + Optional(modifier) + Optional(space + location + space)
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import re
|
|||
|
||||
from certbot.plugins import common
|
||||
|
||||
REDIRECT_DIRECTIVES = ['return', 'rewrite']
|
||||
|
||||
class Addr(common.Addr):
|
||||
r"""Represents an Nginx address, i.e. what comes after the 'listen'
|
||||
|
|
@ -28,10 +29,14 @@ class Addr(common.Addr):
|
|||
:param bool default: Whether the directive includes 'default_server'
|
||||
|
||||
"""
|
||||
UNSPECIFIED_IPV4_ADDRESSES = ('', '*', '0.0.0.0')
|
||||
CANONICAL_UNSPECIFIED_ADDRESS = UNSPECIFIED_IPV4_ADDRESSES[0]
|
||||
|
||||
def __init__(self, host, port, ssl, default):
|
||||
super(Addr, self).__init__((host, port))
|
||||
self.ssl = ssl
|
||||
self.default = default
|
||||
self.unspecified_address = host in self.UNSPECIFIED_IPV4_ADDRESSES
|
||||
|
||||
@classmethod
|
||||
def fromstring(cls, str_addr):
|
||||
|
|
@ -92,9 +97,23 @@ class Addr(common.Addr):
|
|||
def __repr__(self):
|
||||
return "Addr(" + self.__str__() + ")"
|
||||
|
||||
def super_eq(self, other):
|
||||
"""Check ip/port equality, with IPv6 support.
|
||||
"""
|
||||
# If both addresses got an unspecified address, then make sure the
|
||||
# host representation in each match when doing the comparison.
|
||||
if self.unspecified_address and other.unspecified_address:
|
||||
return common.Addr((self.CANONICAL_UNSPECIFIED_ADDRESS,
|
||||
self.tup[1]), self.ipv6) == \
|
||||
common.Addr((other.CANONICAL_UNSPECIFIED_ADDRESS,
|
||||
other.tup[1]), other.ipv6)
|
||||
# Nginx plugin currently doesn't support IPv6 but this will
|
||||
# future-proof it
|
||||
return super(Addr, self).__eq__(other)
|
||||
|
||||
def __eq__(self, other):
|
||||
if isinstance(other, self.__class__):
|
||||
return (self.tup == other.tup and
|
||||
return (self.super_eq(other) and
|
||||
self.ssl == other.ssl and
|
||||
self.default == other.default)
|
||||
return False
|
||||
|
|
@ -149,3 +168,32 @@ class VirtualHost(object): # pylint: disable=too-few-public-methods
|
|||
self.path == other.path)
|
||||
|
||||
return False
|
||||
|
||||
def has_redirect(self):
|
||||
"""Determine if this vhost has a redirecting statement
|
||||
"""
|
||||
for directive_name in REDIRECT_DIRECTIVES:
|
||||
found = _find_directive(self.raw, directive_name)
|
||||
if found is not None:
|
||||
return True
|
||||
return False
|
||||
|
||||
def contains_list(self, test):
|
||||
"""Determine if raw server block contains test list at top level
|
||||
"""
|
||||
for i in xrange(0, len(self.raw) - len(test)):
|
||||
if self.raw[i:i + len(test)] == test:
|
||||
return True
|
||||
return False
|
||||
|
||||
def _find_directive(directives, directive_name):
|
||||
"""Find a directive of type directive_name in directives
|
||||
"""
|
||||
if not directives or isinstance(directives, str) or len(directives) == 0:
|
||||
return None
|
||||
|
||||
if directives[0] == directive_name:
|
||||
return directives
|
||||
|
||||
matches = (_find_directive(line, directive_name) for line in directives)
|
||||
return next((m for m in matches if m is not None), None)
|
||||
|
|
|
|||
|
|
@ -82,21 +82,28 @@ class NginxParser(object):
|
|||
else:
|
||||
return path
|
||||
|
||||
def get_vhosts(self):
|
||||
# pylint: disable=cell-var-from-loop
|
||||
"""Gets list of all 'virtual hosts' found in Nginx configuration.
|
||||
Technically this is a misnomer because Nginx does not have virtual
|
||||
hosts, it has 'server blocks'.
|
||||
|
||||
:returns: List of :class:`~certbot_nginx.obj.VirtualHost`
|
||||
objects found in configuration
|
||||
:rtype: list
|
||||
|
||||
def _build_addr_to_ssl(self):
|
||||
"""Builds a map from address to whether it listens on ssl in any server block
|
||||
"""
|
||||
enabled = True # We only look at enabled vhosts for now
|
||||
vhosts = []
|
||||
servers = {}
|
||||
servers = self._get_raw_servers()
|
||||
|
||||
addr_to_ssl = {}
|
||||
for filename in servers:
|
||||
for server, _ in servers[filename]:
|
||||
# Parse the server block to save addr info
|
||||
parsed_server = _parse_server_raw(server)
|
||||
for addr in parsed_server['addrs']:
|
||||
addr_tuple = addr.normalized_tuple()
|
||||
if addr_tuple not in addr_to_ssl:
|
||||
addr_to_ssl[addr_tuple] = addr.ssl
|
||||
addr_to_ssl[addr_tuple] = addr.ssl or addr_to_ssl[addr_tuple]
|
||||
return addr_to_ssl
|
||||
|
||||
def _get_raw_servers(self):
|
||||
# pylint: disable=cell-var-from-loop
|
||||
"""Get a map of unparsed all server blocks
|
||||
"""
|
||||
servers = {}
|
||||
for filename in self.parsed:
|
||||
tree = self.parsed[filename]
|
||||
servers[filename] = []
|
||||
|
|
@ -110,12 +117,28 @@ class NginxParser(object):
|
|||
for i, (server, path) in enumerate(servers[filename]):
|
||||
new_server = self._get_included_directives(server)
|
||||
servers[filename][i] = (new_server, path)
|
||||
return servers
|
||||
|
||||
def get_vhosts(self):
|
||||
# pylint: disable=cell-var-from-loop
|
||||
"""Gets list of all 'virtual hosts' found in Nginx configuration.
|
||||
Technically this is a misnomer because Nginx does not have virtual
|
||||
hosts, it has 'server blocks'.
|
||||
|
||||
:returns: List of :class:`~certbot_nginx.obj.VirtualHost`
|
||||
objects found in configuration
|
||||
:rtype: list
|
||||
|
||||
"""
|
||||
enabled = True # We only look at enabled vhosts for now
|
||||
servers = self._get_raw_servers()
|
||||
|
||||
vhosts = []
|
||||
for filename in servers:
|
||||
for server, path in servers[filename]:
|
||||
# Parse the server block into a VirtualHost object
|
||||
|
||||
parsed_server = parse_server(server)
|
||||
parsed_server = _parse_server_raw(server)
|
||||
vhost = obj.VirtualHost(filename,
|
||||
parsed_server['addrs'],
|
||||
parsed_server['ssl'],
|
||||
|
|
@ -125,8 +148,20 @@ class NginxParser(object):
|
|||
path)
|
||||
vhosts.append(vhost)
|
||||
|
||||
self._update_vhosts_addrs_ssl(vhosts)
|
||||
|
||||
return vhosts
|
||||
|
||||
def _update_vhosts_addrs_ssl(self, vhosts):
|
||||
"""Update a list of raw parsed vhosts to include global address sslishness
|
||||
"""
|
||||
addr_to_ssl = self._build_addr_to_ssl()
|
||||
for vhost in vhosts:
|
||||
for addr in vhost.addrs:
|
||||
addr.ssl = addr_to_ssl[addr.normalized_tuple()]
|
||||
if addr.ssl:
|
||||
vhost.ssl = True
|
||||
|
||||
def _get_included_directives(self, block):
|
||||
"""Returns array with the "include" directives expanded out by
|
||||
concatenating the contents of the included file to the block.
|
||||
|
|
@ -241,6 +276,17 @@ class NginxParser(object):
|
|||
except IOError:
|
||||
logger.error("Could not open file for writing: %s", filename)
|
||||
|
||||
def parse_server(self, server):
|
||||
"""Parses a list of server directives, accounting for global address sslishness.
|
||||
|
||||
:param list server: list of directives in a server block
|
||||
:rtype: dict
|
||||
"""
|
||||
addr_to_ssl = self._build_addr_to_ssl()
|
||||
parsed_server = _parse_server_raw(server)
|
||||
_apply_global_addr_ssl(addr_to_ssl, parsed_server)
|
||||
return parsed_server
|
||||
|
||||
def has_ssl_on_directive(self, vhost):
|
||||
"""Does vhost have ssl on for all ports?
|
||||
|
||||
|
|
@ -290,7 +336,7 @@ class NginxParser(object):
|
|||
|
||||
# update vhost based on new directives
|
||||
new_server = self._get_included_directives(result)
|
||||
parsed_server = parse_server(new_server)
|
||||
parsed_server = self.parse_server(new_server)
|
||||
vhost.addrs = parsed_server['addrs']
|
||||
vhost.ssl = parsed_server['ssl']
|
||||
vhost.names = parsed_server['names']
|
||||
|
|
@ -298,33 +344,6 @@ class NginxParser(object):
|
|||
except errors.MisconfigurationError as err:
|
||||
raise errors.MisconfigurationError("Problem in %s: %s" % (filename, err.message))
|
||||
|
||||
def get_all_certs_keys(self):
|
||||
"""Gets all certs and keys in the nginx config.
|
||||
|
||||
:returns: list of tuples with form [(cert, key, path)]
|
||||
cert - str path to certificate file
|
||||
key - str path to associated key file
|
||||
path - File path to configuration file.
|
||||
:rtype: set
|
||||
|
||||
"""
|
||||
c_k = set()
|
||||
vhosts = self.get_vhosts()
|
||||
for vhost in vhosts:
|
||||
tup = [None, None, vhost.filep]
|
||||
if vhost.ssl:
|
||||
for directive in vhost.raw:
|
||||
# A directive can be an empty list to preserve whitespace
|
||||
if not directive:
|
||||
continue
|
||||
if directive[0] == 'ssl_certificate':
|
||||
tup[0] = directive[1]
|
||||
elif directive[0] == 'ssl_certificate_key':
|
||||
tup[1] = directive[1]
|
||||
if tup[0] is not None and tup[1] is not None:
|
||||
c_k.add(tuple(tup))
|
||||
return c_k
|
||||
|
||||
|
||||
def _do_for_subarray(entry, condition, func, path=None):
|
||||
"""Executes a function for a subarray of a nested array if it matches
|
||||
|
|
@ -461,42 +480,6 @@ def _get_servernames(names):
|
|||
names = re.sub(whitespace_re, ' ', names)
|
||||
return names.split(' ')
|
||||
|
||||
|
||||
def parse_server(server):
|
||||
"""Parses a list of server directives.
|
||||
|
||||
:param list server: list of directives in a server block
|
||||
:rtype: dict
|
||||
|
||||
"""
|
||||
parsed_server = {'addrs': set(),
|
||||
'ssl': False,
|
||||
'names': set()}
|
||||
|
||||
apply_ssl_to_all_addrs = False
|
||||
|
||||
for directive in server:
|
||||
if not directive:
|
||||
continue
|
||||
if directive[0] == 'listen':
|
||||
addr = obj.Addr.fromstring(directive[1])
|
||||
parsed_server['addrs'].add(addr)
|
||||
if not parsed_server['ssl'] and addr.ssl:
|
||||
parsed_server['ssl'] = True
|
||||
elif directive[0] == 'server_name':
|
||||
parsed_server['names'].update(
|
||||
_get_servernames(directive[1]))
|
||||
elif directive[0] == 'ssl' and directive[1] == 'on':
|
||||
parsed_server['ssl'] = True
|
||||
apply_ssl_to_all_addrs = True
|
||||
|
||||
if apply_ssl_to_all_addrs:
|
||||
for addr in parsed_server['addrs']:
|
||||
addr.ssl = True
|
||||
|
||||
return parsed_server
|
||||
|
||||
|
||||
def _add_directives(block, directives, replace):
|
||||
"""Adds or replaces directives in a config block.
|
||||
|
||||
|
|
@ -577,3 +560,44 @@ def _add_directive(block, directive, replace):
|
|||
'tried to insert directive "{0}" but found '
|
||||
'conflicting "{1}".'.format(directive, block[location]))
|
||||
|
||||
def _apply_global_addr_ssl(addr_to_ssl, parsed_server):
|
||||
"""Apply global sslishness information to the parsed server block
|
||||
"""
|
||||
for addr in parsed_server['addrs']:
|
||||
addr.ssl = addr_to_ssl[addr.normalized_tuple()]
|
||||
if addr.ssl:
|
||||
parsed_server['ssl'] = True
|
||||
|
||||
def _parse_server_raw(server):
|
||||
"""Parses a list of server directives.
|
||||
|
||||
:param list server: list of directives in a server block
|
||||
:rtype: dict
|
||||
|
||||
"""
|
||||
parsed_server = {'addrs': set(),
|
||||
'ssl': False,
|
||||
'names': set()}
|
||||
|
||||
apply_ssl_to_all_addrs = False
|
||||
|
||||
for directive in server:
|
||||
if not directive:
|
||||
continue
|
||||
if directive[0] == 'listen':
|
||||
addr = obj.Addr.fromstring(directive[1])
|
||||
parsed_server['addrs'].add(addr)
|
||||
if addr.ssl:
|
||||
parsed_server['ssl'] = True
|
||||
elif directive[0] == 'server_name':
|
||||
parsed_server['names'].update(
|
||||
_get_servernames(directive[1]))
|
||||
elif directive[0] == 'ssl' and directive[1] == 'on':
|
||||
parsed_server['ssl'] = True
|
||||
apply_ssl_to_all_addrs = True
|
||||
|
||||
if apply_ssl_to_all_addrs:
|
||||
for addr in parsed_server['addrs']:
|
||||
addr.ssl = True
|
||||
|
||||
return parsed_server
|
||||
|
|
|
|||
|
|
@ -21,6 +21,8 @@ from certbot_nginx.tests import util
|
|||
class NginxConfiguratorTest(util.NginxTest):
|
||||
"""Test a semi complex vhost configuration."""
|
||||
|
||||
_multiprocess_can_split_ = True
|
||||
|
||||
def setUp(self):
|
||||
super(NginxConfiguratorTest, self).setUp()
|
||||
|
||||
|
|
@ -40,7 +42,7 @@ class NginxConfiguratorTest(util.NginxTest):
|
|||
|
||||
def test_prepare(self):
|
||||
self.assertEqual((1, 6, 2), self.config.version)
|
||||
self.assertEqual(7, len(self.config.parser.parsed))
|
||||
self.assertEqual(8, len(self.config.parser.parsed))
|
||||
# ensure we successfully parsed a file for ssl_options
|
||||
self.assertTrue(self.config.parser.loc["ssl_options"])
|
||||
|
||||
|
|
@ -68,7 +70,8 @@ class NginxConfiguratorTest(util.NginxTest):
|
|||
names = self.config.get_all_names()
|
||||
self.assertEqual(names, set(
|
||||
["155.225.50.69.nephoscale.net", "www.example.org", "another.alias",
|
||||
"migration.com", "summer.com", "geese.com", "sslon.com"]))
|
||||
"migration.com", "summer.com", "geese.com", "sslon.com",
|
||||
"globalssl.com", "globalsslsetssl.com"]))
|
||||
|
||||
def test_supported_enhancements(self):
|
||||
self.assertEqual(['redirect', 'staple-ocsp'],
|
||||
|
|
@ -155,6 +158,18 @@ class NginxConfiguratorTest(util.NginxTest):
|
|||
"example/chain.pem",
|
||||
None)
|
||||
|
||||
@mock.patch('certbot_nginx.parser.NginxParser.add_server_directives')
|
||||
def test_deploy_cert_raise_on_add_error(self, mock_add_server_directives):
|
||||
mock_add_server_directives.side_effect = errors.MisconfigurationError()
|
||||
self.assertRaises(
|
||||
errors.PluginError,
|
||||
self.config.deploy_cert,
|
||||
"migration.com",
|
||||
"example/cert.pem",
|
||||
"example/key.pem",
|
||||
"example/chain.pem",
|
||||
"example/fullchain.pem")
|
||||
|
||||
def test_deploy_cert(self):
|
||||
server_conf = self.config.parser.abs_path('server.conf')
|
||||
nginx_conf = self.config.parser.abs_path('nginx.conf')
|
||||
|
|
@ -238,41 +253,6 @@ class NginxConfiguratorTest(util.NginxTest):
|
|||
],
|
||||
parsed_migration_conf[0])
|
||||
|
||||
def test_get_all_certs_keys(self):
|
||||
nginx_conf = self.config.parser.abs_path('nginx.conf')
|
||||
example_conf = self.config.parser.abs_path('sites-enabled/example.com')
|
||||
migration_conf = self.config.parser.abs_path('sites-enabled/migration.com')
|
||||
sslon_conf = self.config.parser.abs_path('sites-enabled/sslon.com')
|
||||
|
||||
# Get the default SSL vhost
|
||||
self.config.deploy_cert(
|
||||
"www.example.com",
|
||||
"example/cert.pem",
|
||||
"example/key.pem",
|
||||
"example/chain.pem",
|
||||
"example/fullchain.pem")
|
||||
self.config.deploy_cert(
|
||||
"another.alias",
|
||||
"/etc/nginx/cert.pem",
|
||||
"/etc/nginx/key.pem",
|
||||
"/etc/nginx/chain.pem",
|
||||
"/etc/nginx/fullchain.pem")
|
||||
self.config.deploy_cert(
|
||||
"migration.com",
|
||||
"migration/cert.pem",
|
||||
"migration/key.pem",
|
||||
"migration/chain.pem",
|
||||
"migration/fullchain.pem")
|
||||
self.config.save()
|
||||
|
||||
self.config.parser.load()
|
||||
self.assertEqual(set([
|
||||
('example/fullchain.pem', 'example/key.pem', example_conf),
|
||||
('/etc/nginx/fullchain.pem', '/etc/nginx/key.pem', nginx_conf),
|
||||
('migration/fullchain.pem', 'migration/key.pem', migration_conf),
|
||||
('snakeoil.cert', 'snakeoil.key', sslon_conf),
|
||||
]), self.config.get_all_certs_keys())
|
||||
|
||||
@mock.patch("certbot_nginx.configurator.tls_sni_01.NginxTlsSni01.perform")
|
||||
@mock.patch("certbot_nginx.configurator.NginxConfigurator.restart")
|
||||
@mock.patch("certbot_nginx.configurator.NginxConfigurator.revert_challenge_config")
|
||||
|
|
@ -377,15 +357,15 @@ class NginxConfiguratorTest(util.NginxTest):
|
|||
mock_popen.side_effect = OSError("Can't find program")
|
||||
self.assertRaises(errors.MisconfigurationError, self.config.restart)
|
||||
|
||||
@mock.patch("certbot.util.run_script")
|
||||
def test_config_test(self, _):
|
||||
self.config.config_test()
|
||||
|
||||
@mock.patch("certbot.util.run_script")
|
||||
def test_config_test_bad_process(self, mock_run_script):
|
||||
mock_run_script.side_effect = errors.SubprocessError
|
||||
self.assertRaises(errors.MisconfigurationError, self.config.config_test)
|
||||
|
||||
@mock.patch("certbot.util.run_script")
|
||||
def test_config_test(self, _):
|
||||
self.config.config_test()
|
||||
|
||||
@mock.patch("certbot.reverter.Reverter.recovery_routine")
|
||||
def test_recovery_routine_throws_error_from_reverter(self, mock_recovery_routine):
|
||||
mock_recovery_routine.side_effect = errors.ReverterError("foo")
|
||||
|
|
@ -445,6 +425,64 @@ class NginxConfiguratorTest(util.NginxTest):
|
|||
generated_conf = self.config.parser.parsed[migration_conf]
|
||||
self.assertTrue(util.contains_at_depth(generated_conf, expected, 2))
|
||||
|
||||
@mock.patch('certbot_nginx.obj.VirtualHost.contains_list')
|
||||
@mock.patch('certbot_nginx.obj.VirtualHost.has_redirect')
|
||||
def test_certbot_redirect_exists(self, mock_has_redirect, mock_contains_list):
|
||||
# Test that we add no redirect statement if there is already a
|
||||
# redirect in the block that is managed by certbot
|
||||
# Has a certbot redirect
|
||||
mock_has_redirect.return_value = True
|
||||
mock_contains_list.return_value = True
|
||||
with mock.patch("certbot_nginx.configurator.logger") as mock_logger:
|
||||
self.config.enhance("www.example.com", "redirect")
|
||||
self.assertEqual(mock_logger.info.call_args[0][0],
|
||||
"Traffic on port %s already redirecting to ssl in %s")
|
||||
|
||||
@mock.patch('certbot_nginx.obj.VirtualHost.contains_list')
|
||||
@mock.patch('certbot_nginx.obj.VirtualHost.has_redirect')
|
||||
def test_non_certbot_redirect_exists(self, mock_has_redirect, mock_contains_list):
|
||||
# Test that we add a redirect as a comment if there is already a
|
||||
# redirect-class statement in the block that isn't managed by certbot
|
||||
example_conf = self.config.parser.abs_path('sites-enabled/example.com')
|
||||
|
||||
# Has a non-Certbot redirect, and has no existing comment
|
||||
mock_contains_list.return_value = False
|
||||
mock_has_redirect.return_value = True
|
||||
with mock.patch("certbot_nginx.configurator.logger") as mock_logger:
|
||||
self.config.enhance("www.example.com", "redirect")
|
||||
self.assertEqual(mock_logger.info.call_args[0][0],
|
||||
"The appropriate server block is already redirecting "
|
||||
"traffic. To enable redirect anyway, uncomment the "
|
||||
"redirect lines in %s.")
|
||||
generated_conf = self.config.parser.parsed[example_conf]
|
||||
expected = [
|
||||
['#', ' Redirect non-https traffic to https'],
|
||||
['#', ' if ($scheme != "https") {'],
|
||||
['#', ' return 301 https://$host$request_uri;'],
|
||||
['#', ' } # managed by Certbot']
|
||||
]
|
||||
for line in expected:
|
||||
self.assertTrue(util.contains_at_depth(generated_conf, line, 2))
|
||||
|
||||
@mock.patch('certbot_nginx.obj.VirtualHost.contains_list')
|
||||
@mock.patch('certbot_nginx.obj.VirtualHost.has_redirect')
|
||||
@mock.patch('certbot_nginx.configurator.NginxConfigurator._has_certbot_redirect_comment')
|
||||
@mock.patch('certbot_nginx.configurator.NginxConfigurator._add_redirect_block')
|
||||
def test_redirect_comment_exists(self, mock_add_redirect_block,
|
||||
mock_has_cb_redirect_comment, mock_has_redirect, mock_contains_list):
|
||||
# Test that we add nothing if there is a non-Certbot redirect and a
|
||||
# preexisting comment
|
||||
# Has a non-Certbot redirect and a comment
|
||||
mock_has_redirect.return_value = True
|
||||
mock_contains_list.return_value = False # self._has_certbot_redirect(vhost):
|
||||
mock_has_cb_redirect_comment.return_value = True
|
||||
|
||||
# assert _add_redirect_block not called
|
||||
with mock.patch("certbot_nginx.configurator.logger") as mock_logger:
|
||||
self.config.enhance("www.example.com", "redirect")
|
||||
self.assertFalse(mock_add_redirect_block.called)
|
||||
self.assertTrue(mock_logger.info.called)
|
||||
|
||||
def test_redirect_dont_enhance(self):
|
||||
# Test that we don't accidentally add redirect to ssl-only block
|
||||
with mock.patch("certbot_nginx.configurator.logger") as mock_logger:
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
"""Test for certbot_nginx.nginxparser."""
|
||||
import copy
|
||||
import operator
|
||||
import os
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
from pyparsing import ParseException
|
||||
|
|
@ -128,44 +128,34 @@ class TestRawNginxParser(unittest.TestCase):
|
|||
[['root', ' ', 'html'],
|
||||
['index', ' ', 'index.html index.htm']]]]]))
|
||||
|
||||
with open(util.get_data_filename('nginx.new.conf'), 'w') as handle:
|
||||
dump(parsed, handle)
|
||||
with open(util.get_data_filename('nginx.new.conf')) as handle:
|
||||
parsed_new = load(handle)
|
||||
try:
|
||||
self.maxDiff = None
|
||||
self.assertEqual(parsed[0], parsed_new[0])
|
||||
self.assertEqual(parsed[1:], parsed_new[1:])
|
||||
finally:
|
||||
os.unlink(util.get_data_filename('nginx.new.conf'))
|
||||
with tempfile.TemporaryFile() as f:
|
||||
dump(parsed, f)
|
||||
f.seek(0)
|
||||
parsed_new = load(f)
|
||||
self.assertEqual(parsed, parsed_new)
|
||||
|
||||
def test_comments(self):
|
||||
with open(util.get_data_filename('minimalistic_comments.conf')) as handle:
|
||||
parsed = load(handle)
|
||||
|
||||
with open(util.get_data_filename('minimalistic_comments.new.conf'), 'w') as handle:
|
||||
dump(parsed, handle)
|
||||
with tempfile.TemporaryFile() as f:
|
||||
dump(parsed, f)
|
||||
f.seek(0)
|
||||
parsed_new = load(f)
|
||||
|
||||
with open(util.get_data_filename('minimalistic_comments.new.conf')) as handle:
|
||||
parsed_new = load(handle)
|
||||
|
||||
try:
|
||||
self.assertEqual(parsed, parsed_new)
|
||||
|
||||
self.assertEqual(parsed_new, [
|
||||
['#', " Use bar.conf when it's a full moon!"],
|
||||
['include', 'foo.conf'],
|
||||
['#', ' Kilroy was here'],
|
||||
['check_status'],
|
||||
[['server'],
|
||||
[['#', ''],
|
||||
['#', " Don't forget to open up your firewall!"],
|
||||
['#', ''],
|
||||
['listen', '1234'],
|
||||
['#', ' listen 80;']]],
|
||||
])
|
||||
finally:
|
||||
os.unlink(util.get_data_filename('minimalistic_comments.new.conf'))
|
||||
self.assertEqual(parsed, parsed_new)
|
||||
self.assertEqual(parsed_new, [
|
||||
['#', " Use bar.conf when it's a full moon!"],
|
||||
['include', 'foo.conf'],
|
||||
['#', ' Kilroy was here'],
|
||||
['check_status'],
|
||||
[['server'],
|
||||
[['#', ''],
|
||||
['#', " Don't forget to open up your firewall!"],
|
||||
['#', ''],
|
||||
['listen', '1234'],
|
||||
['#', ' listen 80;']]],
|
||||
])
|
||||
|
||||
def test_issue_518(self):
|
||||
parsed = loads('if ($http_accept ~* "webp") { set $webp "true"; }')
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
"""Test the helper objects in certbot_nginx.obj."""
|
||||
import unittest
|
||||
import itertools
|
||||
|
||||
|
||||
class AddrTest(unittest.TestCase):
|
||||
|
|
@ -72,6 +73,24 @@ class AddrTest(unittest.TestCase):
|
|||
self.assertNotEqual(self.addr1, self.addr2)
|
||||
self.assertFalse(self.addr1 == 3333)
|
||||
|
||||
def test_equivalent_any_addresses(self):
|
||||
from certbot_nginx.obj import Addr
|
||||
any_addresses = ("0.0.0.0:80 default_server ssl",
|
||||
"80 default_server ssl",
|
||||
"*:80 default_server ssl")
|
||||
for first, second in itertools.combinations(any_addresses, 2):
|
||||
self.assertEqual(Addr.fromstring(first), Addr.fromstring(second))
|
||||
|
||||
# Also, make sure ports are checked.
|
||||
self.assertNotEqual(Addr.fromstring(any_addresses[0]),
|
||||
Addr.fromstring("0.0.0.0:443 default_server ssl"))
|
||||
|
||||
# And they aren't equivalent to a specified address.
|
||||
for any_address in any_addresses:
|
||||
self.assertNotEqual(
|
||||
Addr.fromstring("192.168.1.2:80 default_server ssl"),
|
||||
Addr.fromstring(any_address))
|
||||
|
||||
def test_set_inclusion(self):
|
||||
from certbot_nginx.obj import Addr
|
||||
set_a = set([self.addr1, self.addr2])
|
||||
|
|
@ -87,10 +106,43 @@ class VirtualHostTest(unittest.TestCase):
|
|||
def setUp(self):
|
||||
from certbot_nginx.obj import VirtualHost
|
||||
from certbot_nginx.obj import Addr
|
||||
raw1 = [
|
||||
['listen', '69.50.225.155:9000'],
|
||||
[['if', '($scheme != "https") '],
|
||||
[['return', '301 https://$host$request_uri']]
|
||||
],
|
||||
['#', ' managed by Certbot']
|
||||
]
|
||||
self.vhost1 = VirtualHost(
|
||||
"filep",
|
||||
set([Addr.fromstring("localhost")]), False, False,
|
||||
set(['localhost']), [], [])
|
||||
set(['localhost']), raw1, [])
|
||||
raw2 = [
|
||||
['listen', '69.50.225.155:9000'],
|
||||
[['if', '($scheme != "https") '],
|
||||
[['return', '301 https://$host$request_uri']]
|
||||
]
|
||||
]
|
||||
self.vhost2 = VirtualHost(
|
||||
"filep",
|
||||
set([Addr.fromstring("localhost")]), False, False,
|
||||
set(['localhost']), raw2, [])
|
||||
raw3 = [
|
||||
['listen', '69.50.225.155:9000'],
|
||||
['rewrite', '^(.*)$ $scheme://www.domain.com$1 permanent;']
|
||||
]
|
||||
self.vhost3 = VirtualHost(
|
||||
"filep",
|
||||
set([Addr.fromstring("localhost")]), False, False,
|
||||
set(['localhost']), raw3, [])
|
||||
raw4 = [
|
||||
['listen', '69.50.225.155:9000'],
|
||||
['server_name', 'return.com']
|
||||
]
|
||||
self.vhost4 = VirtualHost(
|
||||
"filp",
|
||||
set([Addr.fromstring("localhost")]), False, False,
|
||||
set(['localhost']), raw4, [])
|
||||
|
||||
def test_eq(self):
|
||||
from certbot_nginx.obj import Addr
|
||||
|
|
@ -110,6 +162,47 @@ class VirtualHostTest(unittest.TestCase):
|
|||
'enabled: False'])
|
||||
self.assertEqual(stringified, str(self.vhost1))
|
||||
|
||||
def test_has_redirect(self):
|
||||
self.assertTrue(self.vhost1.has_redirect())
|
||||
self.assertTrue(self.vhost2.has_redirect())
|
||||
self.assertTrue(self.vhost3.has_redirect())
|
||||
self.assertFalse(self.vhost4.has_redirect())
|
||||
|
||||
def test_contains_list(self):
|
||||
from certbot_nginx.obj import VirtualHost
|
||||
from certbot_nginx.obj import Addr
|
||||
from certbot_nginx.configurator import TEST_REDIRECT_BLOCK
|
||||
test_needle = TEST_REDIRECT_BLOCK
|
||||
test_haystack = [['listen', '80'], ['root', '/var/www/html'],
|
||||
['index', 'index.html index.htm index.nginx-debian.html'],
|
||||
['server_name', 'two.functorkitten.xyz'], ['listen', '443 ssl'],
|
||||
['#', ' managed by Certbot'],
|
||||
['ssl_certificate', '/etc/letsencrypt/live/two.functorkitten.xyz/fullchain.pem'],
|
||||
['#', ' managed by Certbot'],
|
||||
['ssl_certificate_key', '/etc/letsencrypt/live/two.functorkitten.xyz/privkey.pem'],
|
||||
['#', ' managed by Certbot'],
|
||||
[['if', '($scheme != "https")'], [['return', '301 https://$host$request_uri']]],
|
||||
['#', ' managed by Certbot'], []]
|
||||
vhost_haystack = VirtualHost(
|
||||
"filp",
|
||||
set([Addr.fromstring("localhost")]), False, False,
|
||||
set(['localhost']), test_haystack, [])
|
||||
test_bad_haystack = [['listen', '80'], ['root', '/var/www/html'],
|
||||
['index', 'index.html index.htm index.nginx-debian.html'],
|
||||
['server_name', 'two.functorkitten.xyz'], ['listen', '443 ssl'],
|
||||
['#', ' managed by Certbot'],
|
||||
['ssl_certificate', '/etc/letsencrypt/live/two.functorkitten.xyz/fullchain.pem'],
|
||||
['#', ' managed by Certbot'],
|
||||
['ssl_certificate_key', '/etc/letsencrypt/live/two.functorkitten.xyz/privkey.pem'],
|
||||
['#', ' managed by Certbot'],
|
||||
[['if', '($scheme != "https")'], [['return', '302 https://$host$request_uri']]],
|
||||
['#', ' managed by Certbot'], []]
|
||||
vhost_bad_haystack = VirtualHost(
|
||||
"filp",
|
||||
set([Addr.fromstring("localhost")]), False, False,
|
||||
set(['localhost']), test_bad_haystack, [])
|
||||
self.assertTrue(vhost_haystack.contains_list(test_needle))
|
||||
self.assertFalse(vhost_bad_haystack.contains_list(test_needle))
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main() # pragma: no cover
|
||||
|
|
|
|||
|
|
@ -49,7 +49,8 @@ class NginxParserTest(util.NginxTest):
|
|||
'sites-enabled/default',
|
||||
'sites-enabled/example.com',
|
||||
'sites-enabled/migration.com',
|
||||
'sites-enabled/sslon.com']]),
|
||||
'sites-enabled/sslon.com',
|
||||
'sites-enabled/globalssl.com']]),
|
||||
set(nparser.parsed.keys()))
|
||||
self.assertEqual([['server_name', 'somename alias another.alias']],
|
||||
nparser.parsed[nparser.abs_path('server.conf')])
|
||||
|
|
@ -73,7 +74,7 @@ class NginxParserTest(util.NginxTest):
|
|||
parsed = nparser._parse_files(nparser.abs_path(
|
||||
'sites-enabled/example.com.test'))
|
||||
self.assertEqual(3, len(glob.glob(nparser.abs_path('*.test'))))
|
||||
self.assertEqual(4, len(
|
||||
self.assertEqual(5, len(
|
||||
glob.glob(nparser.abs_path('sites-enabled/*.test'))))
|
||||
self.assertEqual([[['server'], [['listen', '69.50.225.155:9000'],
|
||||
['listen', '127.0.0.1'],
|
||||
|
|
@ -104,6 +105,16 @@ class NginxParserTest(util.NginxTest):
|
|||
lambda x, y, pts=paths: pts.append(y))
|
||||
self.assertEqual(paths, result)
|
||||
|
||||
def test_get_vhosts_global_ssl(self):
|
||||
nparser = parser.NginxParser(self.config_path, self.ssl_options)
|
||||
vhosts = nparser.get_vhosts()
|
||||
|
||||
vhost = obj.VirtualHost(nparser.abs_path('sites-enabled/globalssl.com'),
|
||||
[obj.Addr('4.8.2.6', '57', True, False)],
|
||||
True, True, set(['globalssl.com']), [], [0])
|
||||
|
||||
globalssl_com = [x for x in vhosts if 'globalssl.com' in x.filep][0]
|
||||
self.assertEqual(vhost, globalssl_com)
|
||||
|
||||
def test_get_vhosts(self):
|
||||
nparser = parser.NginxParser(self.config_path, self.ssl_options)
|
||||
|
|
@ -137,7 +148,7 @@ class NginxParserTest(util.NginxTest):
|
|||
'*.www.example.com']),
|
||||
[], [2, 1, 0])
|
||||
|
||||
self.assertEqual(8, len(vhosts))
|
||||
self.assertEqual(10, len(vhosts))
|
||||
example_com = [x for x in vhosts if 'example.com' in x.filep][0]
|
||||
self.assertEqual(vhost3, example_com)
|
||||
default = [x for x in vhosts if 'default' in x.filep][0]
|
||||
|
|
@ -291,47 +302,34 @@ class NginxParserTest(util.NginxTest):
|
|||
COMMENT_BLOCK,
|
||||
["\n", "e", " ", "f"]])
|
||||
|
||||
def test_get_all_certs_keys(self):
|
||||
nparser = parser.NginxParser(self.config_path, self.ssl_options)
|
||||
filep = nparser.abs_path('sites-enabled/example.com')
|
||||
mock_vhost = obj.VirtualHost(filep,
|
||||
None, None, None,
|
||||
set(['.example.com', 'example.*']),
|
||||
None, [0])
|
||||
nparser.add_server_directives(mock_vhost,
|
||||
[['ssl_certificate', 'foo.pem'],
|
||||
['ssl_certificate_key', 'bar.key'],
|
||||
['listen', '443 ssl']],
|
||||
replace=False)
|
||||
c_k = nparser.get_all_certs_keys()
|
||||
migration_file = nparser.abs_path('sites-enabled/migration.com')
|
||||
sslon_file = nparser.abs_path('sites-enabled/sslon.com')
|
||||
self.assertEqual(set([('foo.pem', 'bar.key', filep),
|
||||
('cert.pem', 'cert.key', migration_file),
|
||||
('snakeoil.cert', 'snakeoil.key', sslon_file)
|
||||
]), c_k)
|
||||
|
||||
def test_parse_server_ssl(self):
|
||||
server = parser.parse_server([
|
||||
def test_parse_server_raw_ssl(self):
|
||||
server = parser._parse_server_raw([ #pylint: disable=protected-access
|
||||
['listen', '443']
|
||||
])
|
||||
self.assertFalse(server['ssl'])
|
||||
|
||||
server = parser.parse_server([
|
||||
server = parser._parse_server_raw([ #pylint: disable=protected-access
|
||||
['listen', '443 ssl']
|
||||
])
|
||||
self.assertTrue(server['ssl'])
|
||||
|
||||
server = parser.parse_server([
|
||||
server = parser._parse_server_raw([ #pylint: disable=protected-access
|
||||
['listen', '443'], ['ssl', 'off']
|
||||
])
|
||||
self.assertFalse(server['ssl'])
|
||||
|
||||
server = parser.parse_server([
|
||||
server = parser._parse_server_raw([ #pylint: disable=protected-access
|
||||
['listen', '443'], ['ssl', 'on']
|
||||
])
|
||||
self.assertTrue(server['ssl'])
|
||||
|
||||
def test_parse_server_global_ssl_applied(self):
|
||||
nparser = parser.NginxParser(self.config_path, self.ssl_options)
|
||||
server = nparser.parse_server([
|
||||
['listen', '443']
|
||||
])
|
||||
self.assertTrue(server['ssl'])
|
||||
|
||||
def test_ssl_options_should_be_parsed_ssl_directives(self):
|
||||
nparser = parser.NginxParser(self.config_path, self.ssl_options)
|
||||
self.assertEqual(nginxparser.UnspacedList(nparser.loc["ssl_options"]),
|
||||
|
|
|
|||
9
certbot-nginx/certbot_nginx/tests/testdata/etc_nginx/sites-enabled/globalssl.com
vendored
Normal file
9
certbot-nginx/certbot_nginx/tests/testdata/etc_nginx/sites-enabled/globalssl.com
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
server {
|
||||
server_name globalssl.com;
|
||||
listen 4.8.2.6:57;
|
||||
}
|
||||
|
||||
server {
|
||||
server_name globalsslsetssl.com;
|
||||
listen 4.8.2.6:57 ssl;
|
||||
}
|
||||
|
|
@ -11,7 +11,7 @@ from acme import jose
|
|||
|
||||
from certbot import configuration
|
||||
|
||||
from certbot.tests import test_util
|
||||
from certbot.tests import util as test_util
|
||||
|
||||
from certbot.plugins import common
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ from setuptools import setup
|
|||
from setuptools import find_packages
|
||||
|
||||
|
||||
version = '0.10.0.dev0'
|
||||
version = '0.11.0.dev0'
|
||||
|
||||
# Please update tox.ini when modifying dependency version requirements
|
||||
install_requires = [
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
"""Certbot client."""
|
||||
|
||||
# version number like 1.2.3a0, must have at least 2 parts, like 1.2
|
||||
__version__ = '0.10.0.dev0'
|
||||
__version__ = '0.11.0.dev0'
|
||||
|
|
|
|||
253
certbot/cert_manager.py
Normal file
253
certbot/cert_manager.py
Normal file
|
|
@ -0,0 +1,253 @@
|
|||
"""Tools for managing certificates."""
|
||||
import datetime
|
||||
import logging
|
||||
import os
|
||||
import pytz
|
||||
import traceback
|
||||
import zope.component
|
||||
|
||||
from certbot import errors
|
||||
from certbot import interfaces
|
||||
from certbot import ocsp
|
||||
from certbot import storage
|
||||
from certbot import util
|
||||
|
||||
from certbot.display import util as display_util
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
###################
|
||||
# Commands
|
||||
###################
|
||||
|
||||
def update_live_symlinks(config):
|
||||
"""Update the certificate file family symlinks to use archive_dir.
|
||||
|
||||
Use the information in the config file to make symlinks point to
|
||||
the correct archive directory.
|
||||
|
||||
.. note:: This assumes that the installation is using a Reverter object.
|
||||
|
||||
:param config: Configuration.
|
||||
:type config: :class:`certbot.configuration.NamespaceConfig`
|
||||
|
||||
"""
|
||||
for renewal_file in storage.renewal_conf_files(config):
|
||||
storage.RenewableCert(renewal_file, config, update_symlinks=True)
|
||||
|
||||
def rename_lineage(config):
|
||||
"""Rename the specified lineage to the new name.
|
||||
|
||||
:param config: Configuration.
|
||||
:type config: :class:`certbot.configuration.NamespaceConfig`
|
||||
|
||||
"""
|
||||
disp = zope.component.getUtility(interfaces.IDisplay)
|
||||
|
||||
certname = _get_certname(config, "rename")
|
||||
|
||||
new_certname = config.new_certname
|
||||
if not new_certname:
|
||||
code, new_certname = disp.input(
|
||||
"Enter the new name for certificate {0}".format(certname),
|
||||
flag="--updated-cert-name", force_interactive=True)
|
||||
if code != display_util.OK or not new_certname:
|
||||
raise errors.Error("User ended interaction.")
|
||||
|
||||
lineage = lineage_for_certname(config, certname)
|
||||
if not lineage:
|
||||
raise errors.ConfigurationError("No existing certificate with name "
|
||||
"{0} found.".format(certname))
|
||||
storage.rename_renewal_config(certname, new_certname, config)
|
||||
disp.notification("Successfully renamed {0} to {1}."
|
||||
.format(certname, new_certname), pause=False)
|
||||
|
||||
def certificates(config):
|
||||
"""Display information about certs configured with Certbot
|
||||
|
||||
:param config: Configuration.
|
||||
:type config: :class:`certbot.configuration.NamespaceConfig`
|
||||
"""
|
||||
parsed_certs = []
|
||||
parse_failures = []
|
||||
for renewal_file in storage.renewal_conf_files(config):
|
||||
try:
|
||||
renewal_candidate = storage.RenewableCert(renewal_file, config)
|
||||
parsed_certs.append(renewal_candidate)
|
||||
except Exception as e: # pylint: disable=broad-except
|
||||
logger.warning("Renewal configuration file %s produced an "
|
||||
"unexpected error: %s. Skipping.", renewal_file, e)
|
||||
logger.debug("Traceback was:\n%s", traceback.format_exc())
|
||||
parse_failures.append(renewal_file)
|
||||
|
||||
# Describe all the certs
|
||||
_describe_certs(config, parsed_certs, parse_failures)
|
||||
|
||||
def delete(config):
|
||||
"""Delete Certbot files associated with a certificate lineage."""
|
||||
certname = _get_certname(config, "delete")
|
||||
storage.delete_files(config, certname)
|
||||
disp = zope.component.getUtility(interfaces.IDisplay)
|
||||
disp.notification("Deleted all files relating to certificate {0}."
|
||||
.format(certname), pause=False)
|
||||
|
||||
###################
|
||||
# Public Helpers
|
||||
###################
|
||||
|
||||
def lineage_for_certname(config, certname):
|
||||
"""Find a lineage object with name certname."""
|
||||
def update_cert_for_name_match(candidate_lineage, rv):
|
||||
"""Return cert if it has name certname, else return rv
|
||||
"""
|
||||
matching_lineage_name_cert = rv
|
||||
if candidate_lineage.lineagename == certname:
|
||||
matching_lineage_name_cert = candidate_lineage
|
||||
return matching_lineage_name_cert
|
||||
return _search_lineages(config, update_cert_for_name_match, None)
|
||||
|
||||
def domains_for_certname(config, certname):
|
||||
"""Find the domains in the cert with name certname."""
|
||||
def update_domains_for_name_match(candidate_lineage, rv):
|
||||
"""Return domains if certname matches, else return rv
|
||||
"""
|
||||
matching_domains = rv
|
||||
if candidate_lineage.lineagename == certname:
|
||||
matching_domains = candidate_lineage.names()
|
||||
return matching_domains
|
||||
return _search_lineages(config, update_domains_for_name_match, None)
|
||||
|
||||
def find_duplicative_certs(config, domains):
|
||||
"""Find existing certs that duplicate the request."""
|
||||
def update_certs_for_domain_matches(candidate_lineage, rv):
|
||||
"""Return cert as identical_names_cert if it matches,
|
||||
or subset_names_cert if it matches as subset
|
||||
"""
|
||||
# TODO: Handle these differently depending on whether they are
|
||||
# expired or still valid?
|
||||
identical_names_cert, subset_names_cert = rv
|
||||
candidate_names = set(candidate_lineage.names())
|
||||
if candidate_names == set(domains):
|
||||
identical_names_cert = candidate_lineage
|
||||
elif candidate_names.issubset(set(domains)):
|
||||
# This logic finds and returns the largest subset-names cert
|
||||
# in the case where there are several available.
|
||||
if subset_names_cert is None:
|
||||
subset_names_cert = candidate_lineage
|
||||
elif len(candidate_names) > len(subset_names_cert.names()):
|
||||
subset_names_cert = candidate_lineage
|
||||
return (identical_names_cert, subset_names_cert)
|
||||
|
||||
return _search_lineages(config, update_certs_for_domain_matches, (None, None))
|
||||
|
||||
|
||||
###################
|
||||
# Private Helpers
|
||||
###################
|
||||
|
||||
def _get_certname(config, verb):
|
||||
"""Get certname from flag, interactively, or error out.
|
||||
"""
|
||||
certname = config.certname
|
||||
if not certname:
|
||||
disp = zope.component.getUtility(interfaces.IDisplay)
|
||||
filenames = storage.renewal_conf_files(config)
|
||||
choices = [storage.lineagename_for_filename(name) for name in filenames]
|
||||
if not choices:
|
||||
raise errors.Error("No existing certificates found.")
|
||||
code, index = disp.menu("Which certificate would you like to {0}?".format(verb),
|
||||
choices, ok_label="Select", flag="--cert-name",
|
||||
force_interactive=True)
|
||||
if code != display_util.OK or not index in range(0, len(choices)):
|
||||
raise errors.Error("User ended interaction.")
|
||||
certname = choices[index]
|
||||
return certname
|
||||
|
||||
def _report_lines(msgs):
|
||||
"""Format a results report for a category of single-line renewal outcomes"""
|
||||
return " " + "\n ".join(str(msg) for msg in msgs)
|
||||
|
||||
def _report_human_readable(config, parsed_certs):
|
||||
"""Format a results report for a parsed cert"""
|
||||
certinfo = []
|
||||
checker = ocsp.RevocationChecker()
|
||||
for cert in parsed_certs:
|
||||
if config.certname and cert.lineagename != config.certname:
|
||||
continue
|
||||
if config.domains and not set(config.domains).issubset(cert.names()):
|
||||
continue
|
||||
now = pytz.UTC.fromutc(datetime.datetime.utcnow())
|
||||
|
||||
reasons = []
|
||||
if cert.is_test_cert:
|
||||
reasons.append('TEST_CERT')
|
||||
if cert.target_expiry <= now:
|
||||
reasons.append('EXPIRED')
|
||||
if checker.ocsp_revoked(cert.cert, cert.chain):
|
||||
reasons.append('REVOKED')
|
||||
|
||||
if reasons:
|
||||
status = "INVALID: " + ", ".join(reasons)
|
||||
else:
|
||||
diff = cert.target_expiry - now
|
||||
if diff.days == 1:
|
||||
status = "VALID: 1 day"
|
||||
elif diff.days < 1:
|
||||
status = "VALID: {0} hour(s)".format(diff.seconds // 3600)
|
||||
else:
|
||||
status = "VALID: {0} days".format(diff.days)
|
||||
|
||||
valid_string = "{0} ({1})".format(cert.target_expiry, status)
|
||||
certinfo.append(" Certificate Name: {0}\n"
|
||||
" Domains: {1}\n"
|
||||
" Expiry Date: {2}\n"
|
||||
" Certificate Path: {3}\n"
|
||||
" Private Key Path: {4}".format(
|
||||
cert.lineagename,
|
||||
" ".join(cert.names()),
|
||||
valid_string,
|
||||
cert.fullchain,
|
||||
cert.privkey))
|
||||
return "\n".join(certinfo)
|
||||
|
||||
def _describe_certs(config, parsed_certs, parse_failures):
|
||||
"""Print information about the certs we know about"""
|
||||
out = []
|
||||
|
||||
notify = out.append
|
||||
|
||||
if not parsed_certs and not parse_failures:
|
||||
notify("No certs found.")
|
||||
else:
|
||||
if parsed_certs:
|
||||
match = "matching " if config.certname or config.domains else ""
|
||||
notify("Found the following {0}certs:".format(match))
|
||||
notify(_report_human_readable(config, parsed_certs))
|
||||
if parse_failures:
|
||||
notify("\nThe following renewal configuration files "
|
||||
"were invalid:")
|
||||
notify(_report_lines(parse_failures))
|
||||
|
||||
disp = zope.component.getUtility(interfaces.IDisplay)
|
||||
disp.notification("\n".join(out), pause=False, wrap=False)
|
||||
|
||||
def _search_lineages(cli_config, func, initial_rv):
|
||||
"""Iterate func over unbroken lineages, allowing custom return conditions.
|
||||
|
||||
Allows flexible customization of return values, including multiple
|
||||
return values and complex checks.
|
||||
"""
|
||||
configs_dir = cli_config.renewal_configs_dir
|
||||
# Verify the directory is there
|
||||
util.make_or_verify_dir(configs_dir, mode=0o755, uid=os.geteuid())
|
||||
|
||||
rv = initial_rv
|
||||
for renewal_file in storage.renewal_conf_files(cli_config):
|
||||
try:
|
||||
candidate_lineage = storage.RenewableCert(renewal_file, cli_config)
|
||||
except (errors.CertStorageError, IOError):
|
||||
logger.debug("Renewal conf file %s is broken. Skipping.", renewal_file)
|
||||
logger.debug("Traceback was:\n%s", traceback.format_exc())
|
||||
continue
|
||||
rv = func(candidate_lineage, rv)
|
||||
return rv
|
||||
483
certbot/cli.py
483
certbot/cli.py
|
|
@ -18,7 +18,6 @@ import certbot
|
|||
from certbot import constants
|
||||
from certbot import crypto_util
|
||||
from certbot import errors
|
||||
from certbot import hooks
|
||||
from certbot import interfaces
|
||||
from certbot import util
|
||||
|
||||
|
|
@ -51,47 +50,55 @@ cli_command = LEAUTO if fragment in sys.argv[0] else "certbot"
|
|||
# to replace as much of it as we can...
|
||||
|
||||
# This is the stub to include in help generated by argparse
|
||||
|
||||
SHORT_USAGE = """
|
||||
{0} [SUBCOMMAND] [options] [-d domain] [-d domain] ...
|
||||
{0} [SUBCOMMAND] [options] [-d DOMAIN] [-d DOMAIN] ...
|
||||
|
||||
Certbot can obtain and install HTTPS/TLS/SSL certificates. By default,
|
||||
it will attempt to use a webserver both for obtaining and installing the
|
||||
cert. Major SUBCOMMANDS are:
|
||||
cert. """.format(cli_command)
|
||||
|
||||
(default) run Obtain & install a cert in your current webserver
|
||||
certonly Obtain cert, but do not install it (aka "auth")
|
||||
install Install a previously obtained cert in a server
|
||||
renew Renew previously obtained certs that are near expiry
|
||||
revoke Revoke a previously obtained certificate
|
||||
register Perform tasks related to registering with the CA
|
||||
rollback Rollback server configuration changes made during install
|
||||
config_changes Show changes made to server config during installation
|
||||
plugins Display information about installed plugins
|
||||
# This section is used for --help and --help all ; it needs information
|
||||
# about installed plugins to be fully formatted
|
||||
COMMAND_OVERVIEW = """The most common SUBCOMMANDS and flags are:
|
||||
|
||||
""".format(cli_command)
|
||||
|
||||
# This is the short help for certbot --help, where we disable argparse
|
||||
# altogether
|
||||
USAGE = SHORT_USAGE + """Choice of server plugins for obtaining and installing cert:
|
||||
obtain, install, and renew certificates:
|
||||
(default) run Obtain & install a cert in your current webserver
|
||||
certonly Obtain or renew a cert, but do not install it
|
||||
renew Renew all previously obtained certs that are near expiry
|
||||
-d DOMAINS Comma-separated list of domains to obtain a cert for
|
||||
|
||||
%s
|
||||
--standalone Run a standalone webserver for authentication
|
||||
%s
|
||||
--webroot Place files in a server's webroot folder for authentication
|
||||
--manual Obtain certs interactively, or using shell script hooks
|
||||
|
||||
OR use different plugins to obtain (authenticate) the cert and then install it:
|
||||
-n Run non-interactively
|
||||
--test-cert Obtain a test cert from a staging server
|
||||
--dry-run Test "renew" or "certonly" without saving any certs to disk
|
||||
|
||||
--authenticator standalone --installer apache
|
||||
manage certificates:
|
||||
certificates Display information about certs you have from Certbot
|
||||
revoke Revoke a certificate (supply --cert-path)
|
||||
delete Delete a certificate
|
||||
|
||||
manage your account with Let's Encrypt:
|
||||
register Create a Let's Encrypt ACME account
|
||||
--agree-tos Agree to the ACME server's Subscriber Agreement
|
||||
-m EMAIL Email address for important account notifications
|
||||
"""
|
||||
|
||||
# This is the short help for certbot --help, where we disable argparse
|
||||
# altogether
|
||||
HELP_USAGE = """
|
||||
More detailed help:
|
||||
|
||||
-h, --help [topic] print this message, or detailed help on a topic;
|
||||
the available topics are:
|
||||
-h, --help [TOPIC] print this message, or detailed help on a topic;
|
||||
the available TOPICS are:
|
||||
|
||||
all, automation, paths, security, testing, or any of the subcommands or
|
||||
plugins (certonly, renew, install, register, nginx, apache, standalone,
|
||||
webroot, etc.)
|
||||
all, automation, commands, paths, security, testing, or any of the
|
||||
subcommands or plugins (certonly, renew, install, register, nginx,
|
||||
apache, standalone, webroot, etc.)
|
||||
"""
|
||||
|
||||
|
||||
|
|
@ -137,19 +144,6 @@ def report_config_interaction(modified, modifiers):
|
|||
VAR_MODIFIERS.setdefault(var, set()).update(modifiers)
|
||||
|
||||
|
||||
def usage_strings(plugins):
|
||||
"""Make usage strings late so that plugins can be initialised late"""
|
||||
if "nginx" in plugins:
|
||||
nginx_doc = "--nginx Use the Nginx plugin for authentication & installation"
|
||||
else:
|
||||
nginx_doc = "(nginx support is experimental, buggy, and not installed by default)"
|
||||
if "apache" in plugins:
|
||||
apache_doc = "--apache Use the Apache plugin for authentication & installation"
|
||||
else:
|
||||
apache_doc = "(the apache plugin is not installed)"
|
||||
return USAGE % (apache_doc, nginx_doc), SHORT_USAGE
|
||||
|
||||
|
||||
def possible_deprecation_warning(config):
|
||||
"A deprecation warning for users with the old, not-self-upgrading letsencrypt-auto."
|
||||
if cli_command != LEAUTO:
|
||||
|
|
@ -305,6 +299,100 @@ class HelpfulArgumentGroup(object):
|
|||
"""Add a new command line argument to the argument group."""
|
||||
self._parser.add(self._topic, *args, **kwargs)
|
||||
|
||||
class CustomHelpFormatter(argparse.HelpFormatter):
|
||||
"""This is a clone of ArgumentDefaultsHelpFormatter, with bugfixes.
|
||||
|
||||
In particular we fix https://bugs.python.org/issue28742
|
||||
"""
|
||||
|
||||
def _get_help_string(self, action):
|
||||
helpstr = action.help
|
||||
if '%(default)' not in action.help and '(default:' not in action.help:
|
||||
if action.default != argparse.SUPPRESS:
|
||||
defaulting_nargs = [argparse.OPTIONAL, argparse.ZERO_OR_MORE]
|
||||
if action.option_strings or action.nargs in defaulting_nargs:
|
||||
helpstr += ' (default: %(default)s)'
|
||||
return helpstr
|
||||
|
||||
# The attributes here are:
|
||||
# short: a string that will be displayed by "certbot -h commands"
|
||||
# opts: a string that heads the section of flags with which this command is documented,
|
||||
# both for "cerbot -h SUBCOMMAND" and "certbot -h all"
|
||||
# usage: an optional string that overrides the header of "certbot -h SUBCOMMAND"
|
||||
VERB_HELP = [
|
||||
("run (default)", {
|
||||
"short": "Obtain/renew a certificate, and install it",
|
||||
"opts": "Options for obtaining & installing certs",
|
||||
"usage": SHORT_USAGE.replace("[SUBCOMMAND]", ""),
|
||||
"realname": "run"
|
||||
}),
|
||||
("certonly", {
|
||||
"short": "Obtain or renew a certificate, but do not install it",
|
||||
"opts": "Options for modifying how a cert is obtained",
|
||||
"usage": ("\n\n certbot certonly [options] [-d DOMAIN] [-d DOMAIN] ...\n\n"
|
||||
"This command obtains a TLS/SSL certificate without installing it anywhere.")
|
||||
}),
|
||||
("renew", {
|
||||
"short": "Renew all certificates (or one specifed with --cert-name)",
|
||||
"opts": ("The 'renew' subcommand will attempt to renew all"
|
||||
" certificates (or more precisely, certificate lineages) you have"
|
||||
" previously obtained if they are close to expiry, and print a"
|
||||
" summary of the results. By default, 'renew' will reuse the options"
|
||||
" used to create obtain or most recently successfully renew each"
|
||||
" certificate lineage. You can try it with `--dry-run` first. For"
|
||||
" more fine-grained control, you can renew individual lineages with"
|
||||
" the `certonly` subcommand. Hooks are available to run commands"
|
||||
" before and after renewal; see"
|
||||
" https://certbot.eff.org/docs/using.html#renewal for more"
|
||||
" information on these."),
|
||||
"usage": "\n\n certbot renew [--cert-name NAME] [options]\n\n"
|
||||
}),
|
||||
("certificates", {
|
||||
"short": "List certificates managed by Certbot",
|
||||
"opts": "List certificates managed by Certbot",
|
||||
"usage": ("\n\n certbot certificates [options] ...\n\n"
|
||||
"Print information about the status of certificates managed by Certbot.")
|
||||
}),
|
||||
("delete", {
|
||||
"short": "Clean up all files related to a certificate",
|
||||
"opts": "Options for deleting a certificate"
|
||||
}),
|
||||
("revoke", {
|
||||
"short": "Revoke a certificate specified with --cert-path",
|
||||
"opts": "Options for revocation of certs",
|
||||
"usage": "\n\n certbot revoke --cert-path /path/to/fullchain.pem [options]\n\n"
|
||||
}),
|
||||
("register", {
|
||||
"short": "Register for account with Let's Encrypt / other ACME server",
|
||||
"opts": "Options for account registration & modification"
|
||||
}),
|
||||
("install", {
|
||||
"short": "Install an arbitrary cert in a server",
|
||||
"opts": "Options for modifying how a cert is deployed"
|
||||
}),
|
||||
("config_changes", {
|
||||
"short": "Show changes that Certbot has made to server configurations",
|
||||
"opts": "Options for controlling which changes are displayed"
|
||||
}),
|
||||
("rollback", {
|
||||
"short": "Roll back server conf changes made during cert installation",
|
||||
"opts": "Options for rolling back server configuration changes"
|
||||
}),
|
||||
("plugins", {
|
||||
"short": "List plugins that are installed and available on your system",
|
||||
"opts": 'Options for for the "plugins" subcommand'
|
||||
}),
|
||||
("update_symlinks", {
|
||||
"short": "Recreate symlinks in your /etc/letsencrypt/live/ directory",
|
||||
"opts": ("Recreates cert and key symlinks in {0}, if you changed them by hand "
|
||||
"or edited a renewal configuration file".format(
|
||||
os.path.join(flag_default("config_dir"), "live")))
|
||||
}),
|
||||
|
||||
]
|
||||
# VERB_HELP is a list in order to preserve order, but a dict is sometimes useful
|
||||
VERB_HELP_MAP = dict(VERB_HELP)
|
||||
|
||||
|
||||
class HelpfulArgumentParser(object):
|
||||
"""Argparse Wrapper.
|
||||
|
|
@ -315,32 +403,34 @@ class HelpfulArgumentParser(object):
|
|||
|
||||
"""
|
||||
|
||||
|
||||
def __init__(self, args, plugins, detect_defaults=False):
|
||||
from certbot import main
|
||||
self.VERBS = {"auth": main.obtain_cert, "certonly": main.obtain_cert,
|
||||
"config_changes": main.config_changes, "run": main.run,
|
||||
"install": main.install, "plugins": main.plugins_cmd,
|
||||
"register": main.register, "renew": main.renew,
|
||||
"revoke": main.revoke, "rollback": main.rollback,
|
||||
"everything": main.run}
|
||||
self.VERBS = {
|
||||
"auth": main.obtain_cert,
|
||||
"certonly": main.obtain_cert,
|
||||
"config_changes": main.config_changes,
|
||||
"run": main.run,
|
||||
"install": main.install,
|
||||
"plugins": main.plugins_cmd,
|
||||
"register": main.register,
|
||||
"renew": main.renew,
|
||||
"revoke": main.revoke,
|
||||
"rollback": main.rollback,
|
||||
"everything": main.run,
|
||||
"update_symlinks": main.update_symlinks,
|
||||
"certificates": main.certificates,
|
||||
"delete": main.delete,
|
||||
}
|
||||
|
||||
# List of topics for which additional help can be provided
|
||||
HELP_TOPICS = ["all", "security", "paths", "automation", "testing"] + list(self.VERBS)
|
||||
HELP_TOPICS += self.COMMANDS_TOPICS + ["manage"]
|
||||
|
||||
plugin_names = list(plugins)
|
||||
self.help_topics = HELP_TOPICS + plugin_names + [None]
|
||||
usage, short_usage = usage_strings(plugins)
|
||||
self.parser = configargparse.ArgParser(
|
||||
usage=short_usage,
|
||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
||||
args_for_setting_config_path=["-c", "--config"],
|
||||
default_config_files=flag_default("config_files"))
|
||||
|
||||
# This is the only way to turn off overly verbose config flag documentation
|
||||
self.parser._add_config_file_help = False # pylint: disable=protected-access
|
||||
|
||||
self.detect_defaults = detect_defaults
|
||||
|
||||
self.args = args
|
||||
self.determine_verb()
|
||||
help1 = self.prescan_for_flag("-h", self.help_topics)
|
||||
|
|
@ -349,13 +439,72 @@ class HelpfulArgumentParser(object):
|
|||
self.help_arg = help1 or help2
|
||||
else:
|
||||
self.help_arg = help1 if isinstance(help1, str) else help2
|
||||
if self.help_arg is True:
|
||||
# just --help with no topic; avoid argparse altogether
|
||||
print(usage)
|
||||
sys.exit(0)
|
||||
|
||||
short_usage = self._usage_string(plugins, self.help_arg)
|
||||
|
||||
self.visible_topics = self.determine_help_topics(self.help_arg)
|
||||
self.groups = {} # elements are added by .add_group()
|
||||
self.defaults = {} # elements are added by .parse_args()
|
||||
self.defaults = {} # elements are added by .parse_args()
|
||||
|
||||
self.parser = configargparse.ArgParser(
|
||||
prog="certbot",
|
||||
usage=short_usage,
|
||||
formatter_class=CustomHelpFormatter,
|
||||
args_for_setting_config_path=["-c", "--config"],
|
||||
default_config_files=flag_default("config_files"),
|
||||
config_arg_help_message="path to config file (default: {0})".format(
|
||||
" and ".join(flag_default("config_files"))))
|
||||
|
||||
# This is the only way to turn off overly verbose config flag documentation
|
||||
self.parser._add_config_file_help = False # pylint: disable=protected-access
|
||||
|
||||
# Help that are synonyms for --help subcommands
|
||||
COMMANDS_TOPICS = ["command", "commands", "subcommand", "subcommands", "verbs"]
|
||||
def _list_subcommands(self):
|
||||
longest = max(len(v) for v in VERB_HELP_MAP.keys())
|
||||
|
||||
text = "The full list of available SUBCOMMANDS is:\n\n"
|
||||
for verb, props in sorted(VERB_HELP):
|
||||
doc = props.get("short", "")
|
||||
text += '{0:<{length}} {1}\n'.format(verb, doc, length=longest)
|
||||
|
||||
text += "\nYou can get more help on a specific subcommand with --help SUBCOMMAND\n"
|
||||
return text
|
||||
|
||||
def _usage_string(self, plugins, help_arg):
|
||||
"""Make usage strings late so that plugins can be initialised late
|
||||
|
||||
:param plugins: all discovered plugins
|
||||
:param help_arg: False for none; True for --help; "TOPIC" for --help TOPIC
|
||||
:rtype: str
|
||||
:returns: a short usage string for the top of --help TOPIC)
|
||||
"""
|
||||
if "nginx" in plugins:
|
||||
nginx_doc = "--nginx Use the Nginx plugin for authentication & installation"
|
||||
else:
|
||||
nginx_doc = "(the certbot nginx plugin is not installed)"
|
||||
if "apache" in plugins:
|
||||
apache_doc = "--apache Use the Apache plugin for authentication & installation"
|
||||
else:
|
||||
apache_doc = "(the cerbot apache plugin is not installed)"
|
||||
|
||||
usage = SHORT_USAGE
|
||||
if help_arg == True:
|
||||
print(usage + COMMAND_OVERVIEW % (apache_doc, nginx_doc) + HELP_USAGE)
|
||||
sys.exit(0)
|
||||
elif help_arg in self.COMMANDS_TOPICS:
|
||||
print(usage + self._list_subcommands())
|
||||
sys.exit(0)
|
||||
elif help_arg == "all":
|
||||
# if we're doing --help all, the OVERVIEW is part of the SHORT_USAGE at
|
||||
# the top; if we're doing --help someothertopic, it's OT so it's not
|
||||
usage += COMMAND_OVERVIEW % (apache_doc, nginx_doc)
|
||||
else:
|
||||
custom = VERB_HELP_MAP.get(help_arg, {}).get("usage", None)
|
||||
usage = custom if custom else usage
|
||||
|
||||
return usage
|
||||
|
||||
|
||||
def parse_args(self):
|
||||
"""Parses command line arguments and returns the result.
|
||||
|
|
@ -376,9 +525,18 @@ class HelpfulArgumentParser(object):
|
|||
|
||||
# Do any post-parsing homework here
|
||||
|
||||
if self.verb == "renew" and not parsed_args.dialog_mode:
|
||||
if self.verb == "renew":
|
||||
if parsed_args.force_interactive:
|
||||
raise errors.Error(
|
||||
"{0} cannot be used with renew".format(
|
||||
constants.FORCE_INTERACTIVE_FLAG))
|
||||
parsed_args.noninteractive_mode = True
|
||||
|
||||
if parsed_args.force_interactive and parsed_args.noninteractive_mode:
|
||||
raise errors.Error(
|
||||
"Flag for non-interactive mode and {0} conflict".format(
|
||||
constants.FORCE_INTERACTIVE_FLAG))
|
||||
|
||||
if parsed_args.staging or parsed_args.dry_run:
|
||||
self.set_test_server(parsed_args)
|
||||
|
||||
|
|
@ -388,20 +546,6 @@ class HelpfulArgumentParser(object):
|
|||
if parsed_args.must_staple:
|
||||
parsed_args.staple = True
|
||||
|
||||
# Avoid conflicting args
|
||||
conficting_args = ["quiet", "noninteractive_mode", "text_mode"]
|
||||
if parsed_args.dialog_mode:
|
||||
for arg in conficting_args:
|
||||
if getattr(parsed_args, arg):
|
||||
raise errors.Error(
|
||||
("Conflicting values for displayer."
|
||||
" {0} conflicts with dialog_mode").format(arg))
|
||||
elif parsed_args.verbose_count > flag_default("verbose_count"):
|
||||
parsed_args.text_mode = True
|
||||
|
||||
if parsed_args.validate_hooks:
|
||||
hooks.validate_hooks(parsed_args)
|
||||
|
||||
return parsed_args
|
||||
|
||||
def set_test_server(self, parsed_args):
|
||||
|
|
@ -571,7 +715,7 @@ class HelpfulArgumentParser(object):
|
|||
util.add_deprecated_argument(
|
||||
self.parser.add_argument, argument_name, num_args)
|
||||
|
||||
def add_group(self, topic, **kwargs):
|
||||
def add_group(self, topic, verbs=(), **kwargs):
|
||||
"""Create a new argument group.
|
||||
|
||||
This method must be called once for every topic, however, calls
|
||||
|
|
@ -579,6 +723,8 @@ class HelpfulArgumentParser(object):
|
|||
clarity.
|
||||
|
||||
:param str topic: Name of the new argument group.
|
||||
:param str verbs: List of subcommands that should be documented as part of
|
||||
this help group / topic
|
||||
|
||||
:returns: The new argument group.
|
||||
:rtype: `HelpfulArgumentGroup`
|
||||
|
|
@ -586,6 +732,9 @@ class HelpfulArgumentParser(object):
|
|||
"""
|
||||
if self.visible_topics[topic]:
|
||||
self.groups[topic] = self.parser.add_argument_group(topic, **kwargs)
|
||||
if self.help_arg:
|
||||
for v in verbs:
|
||||
self.groups[topic].add_argument(v, help=VERB_HELP_MAP[v]["short"])
|
||||
|
||||
return HelpfulArgumentGroup(self, topic)
|
||||
|
||||
|
|
@ -597,7 +746,8 @@ class HelpfulArgumentParser(object):
|
|||
|
||||
"""
|
||||
for name, plugin_ep in six.iteritems(plugins):
|
||||
parser_or_group = self.add_group(name, description=plugin_ep.description)
|
||||
parser_or_group = self.add_group(name,
|
||||
description=plugin_ep.long_description)
|
||||
plugin_ep.plugin_cls.inject_parser_options(parser_or_group, name)
|
||||
|
||||
def determine_help_topics(self, chosen_topic):
|
||||
|
|
@ -625,32 +775,17 @@ class HelpfulArgumentParser(object):
|
|||
def _add_all_groups(helpful):
|
||||
helpful.add_group("automation", description="Arguments for automating execution & other tweaks")
|
||||
helpful.add_group("security", description="Security parameters & server settings")
|
||||
helpful.add_group(
|
||||
"testing", description="The following flags are meant for "
|
||||
"testing purposes only! Do NOT change them, unless you "
|
||||
"really know what you're doing!")
|
||||
# VERBS
|
||||
helpful.add_group(
|
||||
"renew", description="The 'renew' subcommand will attempt to renew all"
|
||||
" certificates (or more precisely, certificate lineages) you have"
|
||||
" previously obtained if they are close to expiry, and print a"
|
||||
" summary of the results. By default, 'renew' will reuse the options"
|
||||
" used to create obtain or most recently successfully renew each"
|
||||
" certificate lineage. You can try it with `--dry-run` first. For"
|
||||
" more fine-grained control, you can renew individual lineages with"
|
||||
" the `certonly` subcommand. Hooks are available to run commands"
|
||||
" before and after renewal; see"
|
||||
" https://certbot.eff.org/docs/using.html#renewal for more"
|
||||
" information on these.")
|
||||
|
||||
helpful.add_group("certonly", description="Options for modifying how a cert is obtained")
|
||||
helpful.add_group("install", description="Options for modifying how a cert is deployed")
|
||||
helpful.add_group("revoke", description="Options for revocation of certs")
|
||||
helpful.add_group("rollback", description="Options for reverting config changes")
|
||||
helpful.add_group("plugins", description='Options for the "plugins" subcommand')
|
||||
helpful.add_group("config_changes",
|
||||
description="Options for showing a history of config changes")
|
||||
helpful.add_group("testing",
|
||||
description="The following flags are meant for testing and integration purposes only.")
|
||||
helpful.add_group("paths", description="Arguments changing execution paths & servers")
|
||||
helpful.add_group("manage",
|
||||
description="Various subcommands and flags are available for managing your certificates:",
|
||||
verbs=["certificates", "delete", "renew", "revoke", "update_symlinks"])
|
||||
|
||||
# VERBS
|
||||
for verb, docs in VERB_HELP:
|
||||
name = docs.get("realname", verb)
|
||||
helpful.add_group(name, description=docs["opts"])
|
||||
|
||||
|
||||
def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: disable=too-many-statements
|
||||
|
|
@ -677,24 +812,33 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis
|
|||
"e.g. -vvv.")
|
||||
helpful.add(
|
||||
None, "-t", "--text", dest="text_mode", action="store_true",
|
||||
help="Use the text output instead of the curses UI.")
|
||||
help=argparse.SUPPRESS)
|
||||
helpful.add(
|
||||
[None, "automation"], "-n", "--non-interactive", "--noninteractive",
|
||||
[None, "automation", "run", "certonly"], "-n", "--non-interactive", "--noninteractive",
|
||||
dest="noninteractive_mode", action="store_true",
|
||||
help="Run without ever asking for user input. This may require "
|
||||
"additional command line flags; the client will try to explain "
|
||||
"which ones are required if it finds one missing")
|
||||
helpful.add(
|
||||
None, "--dialog", dest="dialog_mode", action="store_true",
|
||||
help="Run using interactive dialog menus")
|
||||
[None, "register", "run", "certonly"],
|
||||
constants.FORCE_INTERACTIVE_FLAG, action="store_true",
|
||||
help="Force Certbot to be interactive even if it detects it's not "
|
||||
"being run in a terminal. This flag cannot be used with the "
|
||||
"renew subcommand.")
|
||||
helpful.add(
|
||||
[None, "run", "certonly"],
|
||||
[None, "run", "certonly", "certificates"],
|
||||
"-d", "--domains", "--domain", dest="domains",
|
||||
metavar="DOMAIN", action=_DomainsAction, default=[],
|
||||
help="Domain names to apply. For multiple domains you can use "
|
||||
"multiple -d flags or enter a comma separated list of domains "
|
||||
"as a parameter.")
|
||||
|
||||
"as a parameter. (default: Ask)")
|
||||
helpful.add(
|
||||
[None, "run", "certonly", "manage", "delete", "certificates"],
|
||||
"--cert-name", dest="certname",
|
||||
metavar="CERTNAME", default=None,
|
||||
help="Certificate name to apply. Only one certificate name can be used "
|
||||
"per Certbot run. To see certificate names, run 'certbot certificates'. "
|
||||
"When creating a new certificate, specifies the new certificate's name.")
|
||||
helpful.add(
|
||||
[None, "testing", "renew", "certonly"],
|
||||
"--dry-run", action="store_true", dest="dry_run",
|
||||
|
|
@ -723,18 +867,18 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis
|
|||
help="With the register verb, indicates that details associated "
|
||||
"with an existing registration, such as the e-mail address, "
|
||||
"should be updated, rather than registering a new account.")
|
||||
helpful.add(None, "-m", "--email", help=config_help("email"))
|
||||
helpful.add(["register", "automation"], "-m", "--email", help=config_help("email"))
|
||||
helpful.add(
|
||||
["automation", "renew", "certonly", "run"],
|
||||
["automation", "certonly", "run"],
|
||||
"--keep-until-expiring", "--keep", "--reinstall",
|
||||
dest="reinstall", action="store_true",
|
||||
help="If the requested cert matches an existing cert, always keep the "
|
||||
"existing one until it is due for renewal (for the "
|
||||
"'run' subcommand this means reinstall the existing cert)")
|
||||
"'run' subcommand this means reinstall the existing cert). (default: Ask)")
|
||||
helpful.add(
|
||||
"automation", "--expand", action="store_true",
|
||||
help="If an existing cert covers some subset of the requested names, "
|
||||
"always expand and replace it with the additional names.")
|
||||
"always expand and replace it with the additional names. (default: Ask)")
|
||||
helpful.add(
|
||||
"automation", "--version", action="version",
|
||||
version="%(prog)s {0}".format(certbot.__version__),
|
||||
|
|
@ -747,6 +891,12 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis
|
|||
"regardless of whether it is near expiry. (Often "
|
||||
"--keep-until-expiring is more appropriate). Also implies "
|
||||
"--expand.")
|
||||
helpful.add(
|
||||
"automation", "--renew-with-new-domains",
|
||||
action="store_true", dest="renew_with_new_domains", help="If a "
|
||||
"certificate already exists for the requested certificate name "
|
||||
"but does not match the requested domains, renew it now, "
|
||||
"regardless of whether it is near expiry.")
|
||||
helpful.add(
|
||||
["automation", "renew", "certonly"],
|
||||
"--allow-subset-of-names", action="store_true",
|
||||
|
|
@ -757,7 +907,7 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis
|
|||
"at this system. This option cannot be used with --csr.")
|
||||
helpful.add(
|
||||
"automation", "--agree-tos", dest="tos", action="store_true",
|
||||
help="Agree to the ACME Subscriber Agreement")
|
||||
help="Agree to the ACME Subscriber Agreement (default: Ask)")
|
||||
helpful.add(
|
||||
"automation", "--account", metavar="ACCOUNT_ID",
|
||||
help="Account ID to use")
|
||||
|
|
@ -771,15 +921,17 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis
|
|||
helpful.add(
|
||||
"automation", "--no-self-upgrade", action="store_true",
|
||||
help="(certbot-auto only) prevent the certbot-auto script from"
|
||||
" upgrading itself to newer released versions")
|
||||
" upgrading itself to newer released versions (default: Upgrade"
|
||||
" automatically)")
|
||||
helpful.add(
|
||||
["automation", "renew", "certonly"],
|
||||
["automation", "renew", "certonly", "run"],
|
||||
"-q", "--quiet", dest="quiet", action="store_true",
|
||||
help="Silence all output except errors. Useful for automation via cron."
|
||||
" Implies --non-interactive.")
|
||||
# overwrites server, handled in HelpfulArgumentParser.parse_args()
|
||||
helpful.add("testing", "--test-cert", "--staging", action='store_true', dest='staging',
|
||||
help='Use the staging server to obtain test (invalid) certs; equivalent'
|
||||
helpful.add(["testing", "revoke", "run"], "--test-cert", "--staging",
|
||||
action='store_true', dest='staging',
|
||||
help='Use the staging server to obtain or revoke test (invalid) certs; equivalent'
|
||||
' to --server ' + constants.STAGING_URI)
|
||||
helpful.add(
|
||||
"testing", "--debug", action="store_true",
|
||||
|
|
@ -790,11 +942,11 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis
|
|||
help=config_help("no_verify_ssl"),
|
||||
default=flag_default("no_verify_ssl"))
|
||||
helpful.add(
|
||||
["certonly", "renew", "run"], "--tls-sni-01-port", type=int,
|
||||
["testing", "standalone", "apache", "nginx"], "--tls-sni-01-port", type=int,
|
||||
default=flag_default("tls_sni_01_port"),
|
||||
help=config_help("tls_sni_01_port"))
|
||||
helpful.add(
|
||||
["certonly", "renew", "run", "manual"], "--http-01-port", type=int,
|
||||
["testing", "standalone", "manual"], "--http-01-port", type=int,
|
||||
dest="http01_port",
|
||||
default=flag_default("http01_port"), help=config_help("http01_port"))
|
||||
helpful.add(
|
||||
|
|
@ -810,11 +962,11 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis
|
|||
helpful.add(
|
||||
"security", "--redirect", action="store_true",
|
||||
help="Automatically redirect all HTTP traffic to HTTPS for the newly "
|
||||
"authenticated vhost.", dest="redirect", default=None)
|
||||
"authenticated vhost. (default: Ask)", dest="redirect", default=None)
|
||||
helpful.add(
|
||||
"security", "--no-redirect", action="store_false",
|
||||
help="Do not automatically redirect all HTTP traffic to HTTPS for the newly "
|
||||
"authenticated vhost.", dest="redirect", default=None)
|
||||
"authenticated vhost. (default: Ask)", dest="redirect", default=None)
|
||||
helpful.add(
|
||||
"security", "--hsts", action="store_true",
|
||||
help="Add the Strict-Transport-Security header to every HTTP response."
|
||||
|
|
@ -822,8 +974,7 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis
|
|||
" Defends against SSL Stripping.", dest="hsts", default=False)
|
||||
helpful.add(
|
||||
"security", "--no-hsts", action="store_false",
|
||||
help="Do not automatically add the Strict-Transport-Security header"
|
||||
" to every HTTP response.", dest="hsts", default=False)
|
||||
help=argparse.SUPPRESS, dest="hsts", default=False)
|
||||
helpful.add(
|
||||
"security", "--uir", action="store_true",
|
||||
help="Add the \"Content-Security-Policy: upgrade-insecure-requests\""
|
||||
|
|
@ -831,9 +982,7 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis
|
|||
" https:// for every http:// resource.", dest="uir", default=None)
|
||||
helpful.add(
|
||||
"security", "--no-uir", action="store_false",
|
||||
help="Do not automatically set the \"Content-Security-Policy:"
|
||||
" upgrade-insecure-requests\" header to every HTTP response.",
|
||||
dest="uir", default=None)
|
||||
help=argparse.SUPPRESS, dest="uir", default=None)
|
||||
helpful.add(
|
||||
"security", "--staple-ocsp", action="store_true",
|
||||
help="Enables OCSP Stapling. A valid OCSP response is stapled to"
|
||||
|
|
@ -841,14 +990,13 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis
|
|||
dest="staple", default=None)
|
||||
helpful.add(
|
||||
"security", "--no-staple-ocsp", action="store_false",
|
||||
help="Do not automatically enable OCSP Stapling.",
|
||||
dest="staple", default=None)
|
||||
help=argparse.SUPPRESS, dest="staple", default=None)
|
||||
helpful.add(
|
||||
"security", "--strict-permissions", action="store_true",
|
||||
help="Require that all configuration files are owned by the current "
|
||||
"user; only needed if your config is somewhere unsafe like /tmp/")
|
||||
helpful.add(
|
||||
["manual", "standalone", "certonly", "renew", "run"],
|
||||
["manual", "standalone", "certonly", "renew"],
|
||||
"--preferred-challenges", dest="pref_challs",
|
||||
action=_PrefChallAction, default=[],
|
||||
help='A sorted, comma delimited list of the preferred challenge to '
|
||||
|
|
@ -865,13 +1013,16 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis
|
|||
" Intended primarily for renewal, where it can be used to temporarily"
|
||||
" shut down a webserver that might conflict with the standalone"
|
||||
" plugin. This will only be called if a certificate is actually to be"
|
||||
" obtained/renewed.")
|
||||
" obtained/renewed. When renewing several certificates that have"
|
||||
" identical pre-hooks, only the first will be executed.")
|
||||
helpful.add(
|
||||
"renew", "--post-hook",
|
||||
help="Command to be run in a shell after attempting to obtain/renew"
|
||||
" certificates. Can be used to deploy renewed certificates, or to"
|
||||
" restart any servers that were stopped by --pre-hook. This is only"
|
||||
" run if an attempt was made to obtain/renew a certificate.")
|
||||
" run if an attempt was made to obtain/renew a certificate. If"
|
||||
" multiple renewed certificates have identical post-hooks, only"
|
||||
" one will be run.")
|
||||
helpful.add(
|
||||
"renew", "--renew-hook",
|
||||
help="Command to be run in a shell once for each successfully renewed"
|
||||
|
|
@ -887,9 +1038,11 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis
|
|||
" see if the programs being run are in the $PATH, so that mistakes can"
|
||||
" be caught early, even when the hooks aren't being run just yet. The"
|
||||
" validation is rather simplistic and fails if you use more advanced"
|
||||
" shell constructs, so you can use this switch to disable it.")
|
||||
" shell constructs, so you can use this switch to disable it."
|
||||
" (default: False)")
|
||||
|
||||
helpful.add_deprecated_argument("--agree-dev-preview", 0)
|
||||
helpful.add_deprecated_argument("--dialog", 0)
|
||||
|
||||
_create_subparsers(helpful)
|
||||
_paths_parser(helpful)
|
||||
|
|
@ -906,18 +1059,24 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis
|
|||
def _create_subparsers(helpful):
|
||||
helpful.add("config_changes", "--num", type=int,
|
||||
help="How many past revisions you want to be displayed")
|
||||
|
||||
from certbot.client import sample_user_agent # avoid import loops
|
||||
helpful.add(
|
||||
None, "--user-agent", default=None,
|
||||
help="Set a custom user agent string for the client. User agent strings allow "
|
||||
"the CA to collect high level statistics about success rates by OS and "
|
||||
"plugin. If you wish to hide your server OS version from the Let's "
|
||||
'Encrypt server, set this to "".')
|
||||
'Encrypt server, set this to "". '
|
||||
'(default: {0})'.format(sample_user_agent()))
|
||||
helpful.add("certonly",
|
||||
"--csr", type=read_file,
|
||||
help="Path to a Certificate Signing Request (CSR) in DER"
|
||||
" format; note that the .csr file *must* contain a Subject"
|
||||
" Alternative Name field for each domain you want certified."
|
||||
" Currently --csr only works with the 'certonly' subcommand'")
|
||||
help="Path to a Certificate Signing Request (CSR) in DER or PEM format."
|
||||
" Currently --csr only works with the 'certonly' subcommand.")
|
||||
helpful.add("revoke",
|
||||
"--reason", dest="reason",
|
||||
choices=CaseInsensitiveList(constants.REVOCATION_REASONS.keys()),
|
||||
action=_EncodeReasonAction, default=0,
|
||||
help="Specify reason for revoking certificate.")
|
||||
helpful.add("rollback",
|
||||
"--checkpoints", type=int, metavar="N",
|
||||
default=flag_default("rollback_checkpoints"),
|
||||
|
|
@ -934,16 +1093,24 @@ def _create_subparsers(helpful):
|
|||
const=interfaces.IInstaller, help="Limit to installer plugins only.")
|
||||
|
||||
|
||||
class CaseInsensitiveList(list):
|
||||
"""A list that will ignore case when searching.
|
||||
|
||||
This class is passed to the `choices` argument of `argparse.add_arguments`
|
||||
through the `helpful` wrapper. It is necessary due to special handling of
|
||||
command line arguments by `set_by_cli` in which the `type_func` is not applied."""
|
||||
def __contains__(self, element):
|
||||
return super(CaseInsensitiveList, self).__contains__(element.lower())
|
||||
|
||||
|
||||
def _paths_parser(helpful):
|
||||
add = helpful.add
|
||||
verb = helpful.verb
|
||||
if verb == "help":
|
||||
verb = helpful.help_arg
|
||||
|
||||
cph = "Path to where cert is saved (with auth --csr), installed from or revoked."
|
||||
section = "paths"
|
||||
if verb in ("install", "revoke", "certonly"):
|
||||
section = verb
|
||||
cph = "Path to where cert is saved (with auth --csr), installed from, or revoked."
|
||||
section = ["paths", "install", "revoke", "certonly", "manage"]
|
||||
if verb == "certonly":
|
||||
add(section, "--cert-path", type=os.path.abspath,
|
||||
default=flag_default("auth_cert_path"), help=cph)
|
||||
|
|
@ -965,7 +1132,7 @@ def _paths_parser(helpful):
|
|||
default_cp = None
|
||||
if verb == "certonly":
|
||||
default_cp = flag_default("auth_chain_path")
|
||||
add("paths", "--fullchain-path", default=default_cp, type=os.path.abspath,
|
||||
add(["install", "paths"], "--fullchain-path", default=default_cp, type=os.path.abspath,
|
||||
help="Accompanying path to a full certificate chain (cert plus chain).")
|
||||
add("paths", "--chain-path", default=default_cp, type=os.path.abspath,
|
||||
help="Accompanying path to a certificate chain.")
|
||||
|
|
@ -988,20 +1155,18 @@ def _plugins_parsing(helpful, plugins):
|
|||
"a particular plugin by setting options provided below. Running "
|
||||
"--help <plugin_name> will list flags specific to that plugin.")
|
||||
|
||||
helpful.add(
|
||||
"plugins", "-a", "--authenticator", help="Authenticator plugin name.")
|
||||
helpful.add(
|
||||
"plugins", "-i", "--installer", help="Installer plugin name (also used to find domains).")
|
||||
helpful.add(
|
||||
"plugins", "--configurator", help="Name of the plugin that is "
|
||||
"both an authenticator and an installer. Should not be used "
|
||||
"together with --authenticator or --installer.")
|
||||
helpful.add(["plugins", "certonly", "run", "install"],
|
||||
helpful.add("plugins", "--configurator",
|
||||
help="Name of the plugin that is both an authenticator and an installer."
|
||||
" Should not be used together with --authenticator or --installer. "
|
||||
"(default: Ask)")
|
||||
helpful.add("plugins", "-a", "--authenticator", help="Authenticator plugin name.")
|
||||
helpful.add("plugins", "-i", "--installer",
|
||||
help="Installer plugin name (also used to find domains).")
|
||||
helpful.add(["plugins", "certonly", "run", "install", "config_changes"],
|
||||
"--apache", action="store_true",
|
||||
help="Obtain and install certs using Apache")
|
||||
helpful.add(["plugins", "certonly", "run", "install"],
|
||||
"--nginx", action="store_true",
|
||||
help="Obtain and install certs using Nginx")
|
||||
helpful.add(["plugins", "certonly", "run", "install", "config_changes"],
|
||||
"--nginx", action="store_true", help="Obtain and install certs using Nginx")
|
||||
helpful.add(["plugins", "certonly"], "--standalone", action="store_true",
|
||||
help='Obtain certs using a "standalone" webserver.')
|
||||
helpful.add(["plugins", "certonly"], "--manual", action="store_true",
|
||||
|
|
@ -1016,6 +1181,15 @@ def _plugins_parsing(helpful, plugins):
|
|||
helpful.add_plugin_args(plugins)
|
||||
|
||||
|
||||
class _EncodeReasonAction(argparse.Action):
|
||||
"""Action class for parsing revocation reason."""
|
||||
|
||||
def __call__(self, parser, namespace, reason, option_string=None):
|
||||
"""Encodes the reason for certificate revocation."""
|
||||
code = constants.REVOCATION_REASONS[reason.lower()]
|
||||
setattr(namespace, self.dest, code)
|
||||
|
||||
|
||||
class _DomainsAction(argparse.Action):
|
||||
"""Action class for parsing domains."""
|
||||
|
||||
|
|
@ -1023,7 +1197,6 @@ class _DomainsAction(argparse.Action):
|
|||
"""Just wrap add_domains in argparseese."""
|
||||
add_domains(namespace, domain)
|
||||
|
||||
|
||||
def add_domains(args_or_config, domains):
|
||||
"""Registers new domains to be used during the current client run.
|
||||
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@ import certbot
|
|||
|
||||
from certbot import account
|
||||
from certbot import auth_handler
|
||||
from certbot import configuration
|
||||
from certbot import constants
|
||||
from certbot import crypto_util
|
||||
from certbot import errors
|
||||
|
|
@ -38,11 +37,11 @@ def acme_from_config_key(config, key):
|
|||
"Wrangle ACME client construction"
|
||||
# TODO: Allow for other alg types besides RS256
|
||||
net = acme_client.ClientNetwork(key, verify_ssl=(not config.no_verify_ssl),
|
||||
user_agent=_determine_user_agent(config))
|
||||
user_agent=determine_user_agent(config))
|
||||
return acme_client.Client(config.server, key=key, net=net)
|
||||
|
||||
|
||||
def _determine_user_agent(config):
|
||||
def determine_user_agent(config):
|
||||
"""
|
||||
Set a user_agent string in the config based on the choice of plugins.
|
||||
(this wasn't knowable at construction time)
|
||||
|
|
@ -59,6 +58,16 @@ def _determine_user_agent(config):
|
|||
ua = config.user_agent
|
||||
return ua
|
||||
|
||||
def sample_user_agent():
|
||||
"Document what this Certbot's user agent string will be like."
|
||||
class DummyConfig(object):
|
||||
"Shim for computing a sample user agent."
|
||||
def __init__(self):
|
||||
self.authenticator = "XXX"
|
||||
self.installer = "YYY"
|
||||
self.user_agent = None
|
||||
return determine_user_agent(DummyConfig())
|
||||
|
||||
|
||||
def register(config, account_storage, tos_cb=None):
|
||||
"""Register new account with an ACME CA.
|
||||
|
|
@ -263,7 +272,7 @@ class Client(object):
|
|||
return (self.obtain_certificate_from_csr(domains, csr, authzr=authzr)
|
||||
+ (key, csr))
|
||||
|
||||
def obtain_and_enroll_certificate(self, domains):
|
||||
def obtain_and_enroll_certificate(self, domains, certname):
|
||||
"""Obtain and enroll certificate.
|
||||
|
||||
Get a new certificate for the specified domains using the specified
|
||||
|
|
@ -272,6 +281,7 @@ class Client(object):
|
|||
|
||||
:param list domains: Domains to request.
|
||||
:param plugins: A PluginsFactory object.
|
||||
:param str certname: Name of new cert
|
||||
|
||||
:returns: A new :class:`certbot.storage.RenewableCert` instance
|
||||
referred to the enrolled cert lineage, False if the cert could not
|
||||
|
|
@ -286,16 +296,17 @@ class Client(object):
|
|||
"Non-standard path(s), might not work with crontab installed "
|
||||
"by your operating system package manager")
|
||||
|
||||
new_name = certname if certname else domains[0]
|
||||
if self.config.dry_run:
|
||||
logger.debug("Dry run: Skipping creating new lineage for %s",
|
||||
domains[0])
|
||||
new_name)
|
||||
return None
|
||||
else:
|
||||
return storage.RenewableCert.new_lineage(
|
||||
domains[0], OpenSSL.crypto.dump_certificate(
|
||||
new_name, OpenSSL.crypto.dump_certificate(
|
||||
OpenSSL.crypto.FILETYPE_PEM, certr.body.wrapped),
|
||||
key.pem, crypto_util.dump_pyopenssl_chain(chain),
|
||||
configuration.RenewerConfiguration(self.config.namespace))
|
||||
self.config)
|
||||
|
||||
def save_certificate(self, certr, chain_cert,
|
||||
cert_path, chain_path, fullchain_path):
|
||||
|
|
@ -322,7 +333,7 @@ class Client(object):
|
|||
self.config.strict_permissions)
|
||||
|
||||
cert_pem = OpenSSL.crypto.dump_certificate(
|
||||
OpenSSL.crypto.FILETYPE_PEM, certr.body.wrapped).decode('ascii')
|
||||
OpenSSL.crypto.FILETYPE_PEM, certr.body.wrapped)
|
||||
|
||||
cert_file, abs_cert_path = _open_pem_file('cert_path', cert_path)
|
||||
|
||||
|
|
@ -365,7 +376,8 @@ class Client(object):
|
|||
|
||||
chain_path = None if chain_path is None else os.path.abspath(chain_path)
|
||||
|
||||
with error_handler.ErrorHandler(self.installer.recovery_routine):
|
||||
msg = ("Unable to install the certificate")
|
||||
with error_handler.ErrorHandler(self._recovery_routine_with_msg, msg):
|
||||
for dom in domains:
|
||||
self.installer.deploy_cert(
|
||||
domain=dom, cert_path=os.path.abspath(cert_path),
|
||||
|
|
@ -383,16 +395,10 @@ class Client(object):
|
|||
# sites may have been enabled / final cleanup
|
||||
self.installer.restart()
|
||||
|
||||
def enhance_config(self, domains, config, chain_path):
|
||||
def enhance_config(self, domains, chain_path):
|
||||
"""Enhance the configuration.
|
||||
|
||||
:param list domains: list of domains to configure
|
||||
|
||||
:ivar config: Namespace typically produced by
|
||||
:meth:`argparse.ArgumentParser.parse_args`.
|
||||
it must have the redirect, hsts and uir attributes.
|
||||
:type namespace: :class:`argparse.Namespace`
|
||||
|
||||
:param chain_path: chain file path
|
||||
:type chain_path: `str` or `None`
|
||||
|
||||
|
|
@ -400,39 +406,34 @@ class Client(object):
|
|||
client.
|
||||
|
||||
"""
|
||||
|
||||
if self.installer is None:
|
||||
logger.warning("No installer is specified, there isn't any "
|
||||
"configuration to enhance.")
|
||||
raise errors.Error("No installer available")
|
||||
|
||||
if config is None:
|
||||
logger.warning("No config is specified.")
|
||||
raise errors.Error("No config available")
|
||||
|
||||
enhanced = False
|
||||
enhancement_info = (
|
||||
("hsts", "ensure-http-header", "Strict-Transport-Security"),
|
||||
("redirect", "redirect", None),
|
||||
("staple", "staple-ocsp", chain_path),
|
||||
("uir", "ensure-http-header", "Upgrade-Insecure-Requests"),)
|
||||
supported = self.installer.supported_enhancements()
|
||||
redirect = config.redirect if "redirect" in supported else False
|
||||
hsts = config.hsts if "ensure-http-header" in supported else False
|
||||
uir = config.uir if "ensure-http-header" in supported else False
|
||||
staple = config.staple if "staple-ocsp" in supported else False
|
||||
|
||||
if redirect is None:
|
||||
redirect = enhancements.ask("redirect")
|
||||
|
||||
if redirect:
|
||||
self.apply_enhancement(domains, "redirect")
|
||||
|
||||
if hsts:
|
||||
self.apply_enhancement(domains, "ensure-http-header",
|
||||
"Strict-Transport-Security")
|
||||
if uir:
|
||||
self.apply_enhancement(domains, "ensure-http-header",
|
||||
"Upgrade-Insecure-Requests")
|
||||
if staple:
|
||||
self.apply_enhancement(domains, "staple-ocsp", chain_path)
|
||||
for config_name, enhancement_name, option in enhancement_info:
|
||||
config_value = getattr(self.config, config_name)
|
||||
if enhancement_name in supported:
|
||||
if config_name == "redirect" and config_value is None:
|
||||
config_value = enhancements.ask(enhancement_name)
|
||||
if config_value:
|
||||
self.apply_enhancement(domains, enhancement_name, option)
|
||||
enhanced = True
|
||||
elif config_value:
|
||||
logger.warning(
|
||||
"Option %s is not supported by the selected installer. "
|
||||
"Skipping enhancement.", config_name)
|
||||
|
||||
msg = ("We were unable to restart web server")
|
||||
if redirect or hsts or uir or staple:
|
||||
if enhanced:
|
||||
with error_handler.ErrorHandler(self._rollback_and_restart, msg):
|
||||
self.installer.restart()
|
||||
|
||||
|
|
@ -595,10 +596,10 @@ def _open_pem_file(cli_arg_path, pem_path):
|
|||
|
||||
"""
|
||||
if cli.set_by_cli(cli_arg_path):
|
||||
return util.safe_open(pem_path, chmod=0o644),\
|
||||
return util.safe_open(pem_path, chmod=0o644, mode="wb"),\
|
||||
os.path.abspath(pem_path)
|
||||
else:
|
||||
uniq = util.unique_file(pem_path, 0o644)
|
||||
uniq = util.unique_file(pem_path, 0o644, "wb")
|
||||
return uniq[0], os.path.abspath(uniq[1])
|
||||
|
||||
def _save_chain(chain_pem, chain_file):
|
||||
|
|
|
|||
|
|
@ -25,9 +25,16 @@ class NamespaceConfig(object):
|
|||
- `csr_dir`
|
||||
- `in_progress_dir`
|
||||
- `key_dir`
|
||||
- `renewer_config_file`
|
||||
- `temp_checkpoint_dir`
|
||||
|
||||
And the following paths are dynamically resolved using
|
||||
:attr:`~certbot.interfaces.IConfig.config_dir` and relative
|
||||
paths defined in :py:mod:`certbot.constants`:
|
||||
|
||||
- `default_archive_dir`
|
||||
- `live_dir`
|
||||
- `renewal_configs_dir`
|
||||
|
||||
:ivar namespace: Namespace typically produced by
|
||||
:meth:`argparse.ArgumentParser.parse_args`.
|
||||
:type namespace: :class:`argparse.Namespace`
|
||||
|
|
@ -85,18 +92,8 @@ class NamespaceConfig(object):
|
|||
new_ns = copy.deepcopy(self.namespace)
|
||||
return type(self)(new_ns)
|
||||
|
||||
|
||||
class RenewerConfiguration(object):
|
||||
"""Configuration wrapper for renewer."""
|
||||
|
||||
def __init__(self, namespace):
|
||||
self.namespace = namespace
|
||||
|
||||
def __getattr__(self, name):
|
||||
return getattr(self.namespace, name)
|
||||
|
||||
@property
|
||||
def archive_dir(self): # pylint: disable=missing-docstring
|
||||
def default_archive_dir(self): # pylint: disable=missing-docstring
|
||||
return os.path.join(self.namespace.config_dir, constants.ARCHIVE_DIR)
|
||||
|
||||
@property
|
||||
|
|
@ -108,11 +105,6 @@ class RenewerConfiguration(object):
|
|||
return os.path.join(
|
||||
self.namespace.config_dir, constants.RENEWAL_CONFIGS_DIR)
|
||||
|
||||
@property
|
||||
def renewer_config_file(self): # pylint: disable=missing-docstring
|
||||
return os.path.join(
|
||||
self.namespace.config_dir, constants.RENEWER_CONFIG_FILENAME)
|
||||
|
||||
|
||||
def check_config_sanity(config):
|
||||
"""Validate command line options and display error message if
|
||||
|
|
|
|||
|
|
@ -35,6 +35,17 @@ CLI_DEFAULTS = dict(
|
|||
)
|
||||
STAGING_URI = "https://acme-staging.api.letsencrypt.org/directory"
|
||||
|
||||
# The set of reasons for revoking a certificate is defined in RFC 5280 in
|
||||
# section 5.3.1. The reasons that users are allowed to submit are restricted to
|
||||
# those accepted by the ACME server implementation. They are listed in
|
||||
# `letsencrypt.boulder.revocation.reasons.go`.
|
||||
REVOCATION_REASONS = {
|
||||
"unspecified": 0,
|
||||
"keycompromise": 1,
|
||||
"affiliationchanged": 3,
|
||||
"superseded": 4,
|
||||
"cessationofoperation": 5}
|
||||
|
||||
"""Defaults for CLI flags and `.IConfig` attributes."""
|
||||
|
||||
QUIET_LOGGING_LEVEL = logging.WARNING
|
||||
|
|
@ -93,5 +104,5 @@ TEMP_CHECKPOINT_DIR = "temp_checkpoint"
|
|||
RENEWAL_CONFIGS_DIR = "renewal"
|
||||
"""Renewal configs directory, relative to `IConfig.config_dir`."""
|
||||
|
||||
RENEWER_CONFIG_FILENAME = "renewer.conf"
|
||||
"""Renewer config file name (relative to `IConfig.config_dir`)."""
|
||||
FORCE_INTERACTIVE_FLAG = "--force-interactive"
|
||||
"""Flag to disable TTY checking in IDisplay."""
|
||||
|
|
|
|||
|
|
@ -53,7 +53,8 @@ def init_save_key(key_size, key_dir, keyname="key-certbot.pem"):
|
|||
# Save file
|
||||
util.make_or_verify_dir(key_dir, 0o700, os.geteuid(),
|
||||
config.strict_permissions)
|
||||
key_f, key_path = util.unique_file(os.path.join(key_dir, keyname), 0o600)
|
||||
key_f, key_path = util.unique_file(
|
||||
os.path.join(key_dir, keyname), 0o600, "wb")
|
||||
with key_f:
|
||||
key_f.write(key_pem)
|
||||
|
||||
|
|
@ -85,7 +86,7 @@ def init_save_csr(privkey, names, path, csrname="csr-certbot.pem"):
|
|||
util.make_or_verify_dir(path, 0o755, os.geteuid(),
|
||||
config.strict_permissions)
|
||||
csr_f, csr_filename = util.unique_file(
|
||||
os.path.join(path, csrname), 0o644)
|
||||
os.path.join(path, csrname), 0o644, "wb")
|
||||
csr_f.write(csr_pem)
|
||||
csr_f.close()
|
||||
|
||||
|
|
@ -351,11 +352,11 @@ def dump_pyopenssl_chain(chain, filetype=OpenSSL.crypto.FILETYPE_PEM):
|
|||
if isinstance(cert, jose.ComparableX509):
|
||||
# pylint: disable=protected-access
|
||||
cert = cert.wrapped
|
||||
return OpenSSL.crypto.dump_certificate(filetype, cert).decode('ascii')
|
||||
return OpenSSL.crypto.dump_certificate(filetype, cert)
|
||||
|
||||
# assumes that OpenSSL.crypto.dump_certificate includes ending
|
||||
# newline character
|
||||
return "".join(_dump_cert(cert) for cert in chain)
|
||||
return b"".join(_dump_cert(cert) for cert in chain)
|
||||
|
||||
|
||||
def notBefore(cert_path):
|
||||
|
|
|
|||
|
|
@ -48,7 +48,8 @@ def redirect_by_default():
|
|||
|
||||
code, selection = util(interfaces.IDisplay).menu(
|
||||
"Please choose whether HTTPS access is required or optional.",
|
||||
choices, default=0, cli_flag="--redirect / --no-redirect")
|
||||
choices, default=0,
|
||||
cli_flag="--redirect / --no-redirect", force_interactive=True)
|
||||
|
||||
if code != display_util.OK:
|
||||
return False
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ def get_email(invalid=False, optional=True):
|
|||
if optional:
|
||||
if invalid:
|
||||
msg += unsafe_suggestion
|
||||
suggest_unsafe = False
|
||||
else:
|
||||
suggest_unsafe = True
|
||||
else:
|
||||
|
|
@ -45,7 +46,8 @@ def get_email(invalid=False, optional=True):
|
|||
while True:
|
||||
try:
|
||||
code, email = z_util(interfaces.IDisplay).input(
|
||||
invalid_prefix + msg if invalid else msg)
|
||||
invalid_prefix + msg if invalid else msg,
|
||||
force_interactive=True)
|
||||
except errors.MissingCommandlineFlag:
|
||||
msg = ("You should register before running non-interactively, "
|
||||
"or provide --agree-tos and --email <email_address> flags.")
|
||||
|
|
@ -78,7 +80,7 @@ def choose_account(accounts):
|
|||
labels = [acc.slug for acc in accounts]
|
||||
|
||||
code, index = z_util(interfaces.IDisplay).menu(
|
||||
"Please choose an account", labels)
|
||||
"Please choose an account", labels, force_interactive=True)
|
||||
if code == display_util.OK:
|
||||
return accounts[index]
|
||||
else:
|
||||
|
|
@ -129,6 +131,16 @@ def get_valid_domains(domains):
|
|||
continue
|
||||
return valid_domains
|
||||
|
||||
def _sort_names(FQDNs):
|
||||
"""Sort FQDNs by SLD (and if many, by their subdomains)
|
||||
|
||||
:param list FQDNs: list of domain names
|
||||
|
||||
:returns: Sorted list of domain names
|
||||
:rtype: list
|
||||
"""
|
||||
return sorted(FQDNs, key=lambda fqdn: fqdn.split('.')[::-1][1:])
|
||||
|
||||
|
||||
def _filter_names(names):
|
||||
"""Determine which names the user would like to select from a list.
|
||||
|
|
@ -141,9 +153,12 @@ def _filter_names(names):
|
|||
:rtype: tuple
|
||||
|
||||
"""
|
||||
#Sort by domain first, and then by subdomain
|
||||
sorted_names = _sort_names(names)
|
||||
|
||||
code, names = z_util(interfaces.IDisplay).checklist(
|
||||
"Which names would you like to activate HTTPS for?",
|
||||
tags=names, cli_flag="--domains")
|
||||
tags=sorted_names, cli_flag="--domains", force_interactive=True)
|
||||
return code, [str(s) for s in names]
|
||||
|
||||
|
||||
|
|
@ -159,7 +174,7 @@ def _choose_names_manually(prompt_prefix=""):
|
|||
code, input_ = z_util(interfaces.IDisplay).input(
|
||||
prompt_prefix +
|
||||
"Please enter in your domain name(s) (comma and/or space separated) ",
|
||||
cli_flag="--domains")
|
||||
cli_flag="--domains", force_interactive=True)
|
||||
|
||||
if code == display_util.OK:
|
||||
invalid_domains = dict()
|
||||
|
|
@ -197,7 +212,8 @@ def _choose_names_manually(prompt_prefix=""):
|
|||
|
||||
if retry_message:
|
||||
# We had error in input
|
||||
retry = z_util(interfaces.IDisplay).yesno(retry_message)
|
||||
retry = z_util(interfaces.IDisplay).yesno(retry_message,
|
||||
force_interactive=True)
|
||||
if retry:
|
||||
return _choose_names_manually()
|
||||
else:
|
||||
|
|
@ -208,8 +224,6 @@ def _choose_names_manually(prompt_prefix=""):
|
|||
def success_installation(domains):
|
||||
"""Display a box confirming the installation of HTTPS.
|
||||
|
||||
.. todo:: This should be centered on the screen
|
||||
|
||||
:param list domains: domain names which were enabled
|
||||
|
||||
"""
|
||||
|
|
@ -219,29 +233,36 @@ def success_installation(domains):
|
|||
_gen_https_names(domains),
|
||||
os.linesep,
|
||||
os.linesep.join(_gen_ssl_lab_urls(domains))),
|
||||
height=(10 + len(domains)),
|
||||
pause=False)
|
||||
|
||||
|
||||
def success_renewal(domains, action):
|
||||
def success_renewal(domains):
|
||||
"""Display a box confirming the renewal of an existing certificate.
|
||||
|
||||
.. todo:: This should be centered on the screen
|
||||
|
||||
:param list domains: domain names which were renewed
|
||||
:param str action: can be "reinstall" or "renew"
|
||||
|
||||
"""
|
||||
z_util(interfaces.IDisplay).notification(
|
||||
"Your existing certificate has been successfully {3}ed, and the "
|
||||
"Your existing certificate has been successfully renewed, and the "
|
||||
"new certificate has been installed.{1}{1}"
|
||||
"The new certificate covers the following domains: {0}{1}{1}"
|
||||
"You should test your configuration at:{1}{2}".format(
|
||||
_gen_https_names(domains),
|
||||
os.linesep,
|
||||
os.linesep.join(_gen_ssl_lab_urls(domains)),
|
||||
action),
|
||||
height=(14 + len(domains)),
|
||||
os.linesep.join(_gen_ssl_lab_urls(domains))),
|
||||
pause=False)
|
||||
|
||||
def success_revocation(cert_path):
|
||||
"""Display a box confirming a certificate has been revoked.
|
||||
|
||||
:param list cert_path: path to certificate which was revoked.
|
||||
|
||||
"""
|
||||
z_util(interfaces.IDisplay).notification(
|
||||
"Congratulations! You have successfully revoked the certificate "
|
||||
"that was located at {0}{1}{1}".format(
|
||||
cert_path,
|
||||
os.linesep),
|
||||
pause=False)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -2,27 +2,19 @@
|
|||
import logging
|
||||
import os
|
||||
import textwrap
|
||||
import sys
|
||||
|
||||
import dialog
|
||||
import six
|
||||
import zope.interface
|
||||
|
||||
from certbot import constants
|
||||
from certbot import interfaces
|
||||
from certbot import errors
|
||||
from certbot.display import completer
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
WIDTH = 72
|
||||
HEIGHT = 20
|
||||
|
||||
DSELECT_HELP = (
|
||||
"Use the arrow keys or Tab to move between window elements. Space can be "
|
||||
"used to complete the input path with the selected element in the "
|
||||
"directory window. Pressing enter will select the currently highlighted "
|
||||
"button.")
|
||||
"""Help text on how to use dialog's dselect."""
|
||||
|
||||
# Display exit codes
|
||||
OK = "ok"
|
||||
|
|
@ -59,198 +51,45 @@ def _wrap_lines(msg):
|
|||
|
||||
return os.linesep.join(fixed_l)
|
||||
|
||||
|
||||
def _clean(dialog_result):
|
||||
"""Treat sundy python-dialog return codes as CANCEL
|
||||
|
||||
:param tuple dialog_result: (code, result)
|
||||
:returns: the argument but with unknown codes set to -1 (Error)
|
||||
:rtype: tuple
|
||||
"""
|
||||
code, result = dialog_result
|
||||
if code in (OK, HELP):
|
||||
return dialog_result
|
||||
elif code in (CANCEL, ESC):
|
||||
return (CANCEL, result)
|
||||
else:
|
||||
logger.debug("Surprising dialog return code %s", code)
|
||||
return (CANCEL, result)
|
||||
|
||||
|
||||
@zope.interface.implementer(interfaces.IDisplay)
|
||||
class NcursesDisplay(object):
|
||||
"""Ncurses-based display."""
|
||||
|
||||
def __init__(self, width=WIDTH, height=HEIGHT):
|
||||
super(NcursesDisplay, self).__init__()
|
||||
self.dialog = dialog.Dialog(autowidgetsize=True)
|
||||
assert OK == self.dialog.DIALOG_OK, "What kind of absurdity is this?"
|
||||
self.width = width
|
||||
self.height = height
|
||||
|
||||
def notification(self, message, height=10, pause=False):
|
||||
# pylint: disable=unused-argument
|
||||
"""Display a notification to the user and wait for user acceptance.
|
||||
|
||||
.. todo:: It probably makes sense to use one of the transient message
|
||||
types for pause. It isn't straightforward how best to approach
|
||||
the matter though given the context of our messages.
|
||||
http://pythondialog.sourceforge.net/doc/widgets.html#displaying-transient-messages
|
||||
|
||||
:param str message: Message to display
|
||||
:param int height: Height of the dialog box
|
||||
:param bool pause: Not applicable to NcursesDisplay
|
||||
|
||||
"""
|
||||
self.dialog.msgbox(message)
|
||||
|
||||
def menu(self, message, choices, ok_label="OK", cancel_label="Cancel",
|
||||
help_label="", **unused_kwargs):
|
||||
"""Display a menu.
|
||||
|
||||
:param str message: title of menu
|
||||
|
||||
:param choices: menu lines, len must be > 0
|
||||
:type choices: list of tuples (`tag`, `item`) tags must be unique or
|
||||
list of items (tags will be enumerated)
|
||||
|
||||
:param str ok_label: label of the OK button
|
||||
:param str help_label: label of the help button
|
||||
:param dict unused_kwargs: absorbs default / cli_args
|
||||
|
||||
:returns: tuple of the form (`code`, `index`) where
|
||||
`code` - display exit code
|
||||
`int` - index of the selected item
|
||||
:rtype: tuple
|
||||
|
||||
"""
|
||||
menu_options = {
|
||||
"choices": choices,
|
||||
"ok_label": ok_label,
|
||||
"cancel_label": cancel_label,
|
||||
"help_button": bool(help_label),
|
||||
"help_label": help_label,
|
||||
"width": self.width,
|
||||
"height": self.height,
|
||||
"menu_height": self.height - 6,
|
||||
}
|
||||
|
||||
# Can accept either tuples or just the actual choices
|
||||
if choices and isinstance(choices[0], tuple):
|
||||
# pylint: disable=star-args
|
||||
code, selection = _clean(self.dialog.menu(message, **menu_options))
|
||||
|
||||
# Return the selection index
|
||||
for i, choice in enumerate(choices):
|
||||
if choice[0] == selection:
|
||||
return code, i
|
||||
|
||||
return code, -1
|
||||
|
||||
else:
|
||||
# "choices" is not formatted the way the dialog.menu expects...
|
||||
menu_options["choices"] = [
|
||||
(str(i), choice) for i, choice in enumerate(choices, 1)
|
||||
]
|
||||
# pylint: disable=star-args
|
||||
code, index = _clean(self.dialog.menu(message, **menu_options))
|
||||
|
||||
if code == CANCEL or index == "":
|
||||
return code, -1
|
||||
|
||||
return code, int(index) - 1
|
||||
|
||||
def input(self, message, **unused_kwargs):
|
||||
"""Display an input box to the user.
|
||||
|
||||
:param str message: Message to display that asks for input.
|
||||
:param dict _kwargs: absorbs default / cli_args
|
||||
|
||||
:returns: tuple of the form (`code`, `string`) where
|
||||
`code` - display exit code
|
||||
`string` - input entered by the user
|
||||
|
||||
"""
|
||||
return self.dialog.inputbox(message)
|
||||
|
||||
def yesno(self, message, yes_label="Yes", no_label="No", **unused_kwargs):
|
||||
"""Display a Yes/No dialog box.
|
||||
|
||||
Yes and No label must begin with different letters.
|
||||
|
||||
:param str message: message to display to user
|
||||
:param str yes_label: label on the "yes" button
|
||||
:param str no_label: label on the "no" button
|
||||
:param dict _kwargs: absorbs default / cli_args
|
||||
|
||||
:returns: if yes_label was selected
|
||||
:rtype: bool
|
||||
|
||||
"""
|
||||
return self.dialog.DIALOG_OK == self.dialog.yesno(
|
||||
message, yes_label=yes_label, no_label=no_label)
|
||||
|
||||
def checklist(self, message, tags, default_status=True, **unused_kwargs):
|
||||
"""Displays a checklist.
|
||||
|
||||
:param message: Message to display before choices
|
||||
:param list tags: where each is of type :class:`str` len(tags) > 0
|
||||
:param bool default_status: If True, items are in a selected state by
|
||||
default.
|
||||
:param dict _kwargs: absorbs default / cli_args
|
||||
|
||||
|
||||
:returns: tuple of the form (`code`, `list_tags`) where
|
||||
`code` - display exit code
|
||||
`list_tags` - list of str tags selected by the user
|
||||
|
||||
"""
|
||||
choices = [(tag, "", default_status) for tag in tags]
|
||||
return self.dialog.checklist(message, choices=choices)
|
||||
|
||||
def directory_select(self, message, **unused_kwargs):
|
||||
"""Display a directory selection screen.
|
||||
|
||||
:param str message: prompt to give the user
|
||||
|
||||
:returns: tuple of the form (`code`, `string`) where
|
||||
`code` - display exit code
|
||||
`string` - input entered by the user
|
||||
|
||||
"""
|
||||
root_directory = os.path.abspath(os.sep)
|
||||
return self.dialog.dselect(
|
||||
filepath=root_directory, help_button=True, title=message)
|
||||
|
||||
|
||||
@zope.interface.implementer(interfaces.IDisplay)
|
||||
class FileDisplay(object):
|
||||
"""File-based display."""
|
||||
# pylint: disable=too-many-arguments
|
||||
# see https://github.com/certbot/certbot/issues/3915
|
||||
|
||||
def __init__(self, outfile):
|
||||
def __init__(self, outfile, force_interactive):
|
||||
super(FileDisplay, self).__init__()
|
||||
self.outfile = outfile
|
||||
self.force_interactive = force_interactive
|
||||
self.skipped_interaction = False
|
||||
|
||||
def notification(self, message, height=10, pause=True):
|
||||
# pylint: disable=unused-argument
|
||||
def notification(self, message, pause=True,
|
||||
wrap=True, force_interactive=False):
|
||||
"""Displays a notification and waits for user acceptance.
|
||||
|
||||
:param str message: Message to display
|
||||
:param int height: No effect for FileDisplay
|
||||
:param bool pause: Whether or not the program should pause for the
|
||||
user's confirmation
|
||||
:param bool wrap: Whether or not the application should wrap text
|
||||
:param bool force_interactive: True if it's safe to prompt the user
|
||||
because it won't cause any workflow regressions
|
||||
|
||||
"""
|
||||
side_frame = "-" * 79
|
||||
message = _wrap_lines(message)
|
||||
if wrap:
|
||||
message = _wrap_lines(message)
|
||||
self.outfile.write(
|
||||
"{line}{frame}{line}{msg}{line}{frame}{line}".format(
|
||||
line=os.linesep, frame=side_frame, msg=message))
|
||||
if pause:
|
||||
six.moves.input("Press Enter to Continue")
|
||||
if self._can_interact(force_interactive):
|
||||
six.moves.input("Press Enter to Continue")
|
||||
else:
|
||||
logger.debug("Not pausing for user confirmation")
|
||||
|
||||
def menu(self, message, choices, ok_label="", cancel_label="",
|
||||
help_label="", **unused_kwargs):
|
||||
help_label="", default=None,
|
||||
cli_flag=None, force_interactive=False, **unused_kwargs):
|
||||
# pylint: disable=unused-argument
|
||||
"""Display a menu.
|
||||
|
||||
|
|
@ -261,7 +100,10 @@ class FileDisplay(object):
|
|||
:param choices: Menu lines, len must be > 0
|
||||
:type choices: list of tuples (tag, item) or
|
||||
list of descriptions (tags will be enumerated)
|
||||
:param dict _kwargs: absorbs default / cli_args
|
||||
:param default: default value to return (if one exists)
|
||||
:param str cli_flag: option used to set this value with the CLI
|
||||
:param bool force_interactive: True if it's safe to prompt the user
|
||||
because it won't cause any workflow regressions
|
||||
|
||||
:returns: tuple of (`code`, `index`) where
|
||||
`code` - str display exit code
|
||||
|
|
@ -270,18 +112,25 @@ class FileDisplay(object):
|
|||
:rtype: tuple
|
||||
|
||||
"""
|
||||
if self._return_default(message, default, cli_flag, force_interactive):
|
||||
return OK, default
|
||||
|
||||
self._print_menu(message, choices)
|
||||
|
||||
code, selection = self._get_valid_int_ans(len(choices))
|
||||
|
||||
return code, selection - 1
|
||||
|
||||
def input(self, message, **unused_kwargs):
|
||||
def input(self, message, default=None,
|
||||
cli_flag=None, force_interactive=False, **unused_kwargs):
|
||||
# pylint: disable=no-self-use
|
||||
"""Accept input from the user.
|
||||
|
||||
:param str message: message to display to the user
|
||||
:param dict _kwargs: absorbs default / cli_args
|
||||
:param default: default value to return (if one exists)
|
||||
:param str cli_flag: option used to set this value with the CLI
|
||||
:param bool force_interactive: True if it's safe to prompt the user
|
||||
because it won't cause any workflow regressions
|
||||
|
||||
:returns: tuple of (`code`, `input`) where
|
||||
`code` - str display exit code
|
||||
|
|
@ -289,6 +138,9 @@ class FileDisplay(object):
|
|||
:rtype: tuple
|
||||
|
||||
"""
|
||||
if self._return_default(message, default, cli_flag, force_interactive):
|
||||
return OK, default
|
||||
|
||||
ans = six.moves.input(
|
||||
textwrap.fill(
|
||||
"%s (Enter 'c' to cancel): " % message,
|
||||
|
|
@ -301,7 +153,8 @@ class FileDisplay(object):
|
|||
else:
|
||||
return OK, ans
|
||||
|
||||
def yesno(self, message, yes_label="Yes", no_label="No", **unused_kwargs):
|
||||
def yesno(self, message, yes_label="Yes", no_label="No", default=None,
|
||||
cli_flag=None, force_interactive=False, **unused_kwargs):
|
||||
"""Query the user with a yes/no question.
|
||||
|
||||
Yes and No label must begin with different letters, and must contain at
|
||||
|
|
@ -310,12 +163,18 @@ class FileDisplay(object):
|
|||
:param str message: question for the user
|
||||
:param str yes_label: Label of the "Yes" parameter
|
||||
:param str no_label: Label of the "No" parameter
|
||||
:param dict _kwargs: absorbs default / cli_args
|
||||
:param default: default value to return (if one exists)
|
||||
:param str cli_flag: option used to set this value with the CLI
|
||||
:param bool force_interactive: True if it's safe to prompt the user
|
||||
because it won't cause any workflow regressions
|
||||
|
||||
:returns: True for "Yes", False for "No"
|
||||
:rtype: bool
|
||||
|
||||
"""
|
||||
if self._return_default(message, default, cli_flag, force_interactive):
|
||||
return default
|
||||
|
||||
side_frame = ("-" * 79) + os.linesep
|
||||
|
||||
message = _wrap_lines(message)
|
||||
|
|
@ -337,14 +196,18 @@ class FileDisplay(object):
|
|||
ans.startswith(no_label[0].upper())):
|
||||
return False
|
||||
|
||||
def checklist(self, message, tags, default_status=True, **unused_kwargs):
|
||||
def checklist(self, message, tags, default_status=True, default=None,
|
||||
cli_flag=None, force_interactive=False, **unused_kwargs):
|
||||
# pylint: disable=unused-argument
|
||||
"""Display a checklist.
|
||||
|
||||
:param str message: Message to display to user
|
||||
:param list tags: `str` tags to select, len(tags) > 0
|
||||
:param bool default_status: Not used for FileDisplay
|
||||
:param dict _kwargs: absorbs default / cli_args
|
||||
:param default: default value to return (if one exists)
|
||||
:param str cli_flag: option used to set this value with the CLI
|
||||
:param bool force_interactive: True if it's safe to prompt the user
|
||||
because it won't cause any workflow regressions
|
||||
|
||||
:returns: tuple of (`code`, `tags`) where
|
||||
`code` - str display exit code
|
||||
|
|
@ -352,13 +215,20 @@ class FileDisplay(object):
|
|||
:rtype: tuple
|
||||
|
||||
"""
|
||||
if self._return_default(message, default, cli_flag, force_interactive):
|
||||
return OK, default
|
||||
|
||||
while True:
|
||||
self._print_menu(message, tags)
|
||||
|
||||
code, ans = self.input("Select the appropriate numbers separated "
|
||||
"by commas and/or spaces")
|
||||
"by commas and/or spaces, or leave input "
|
||||
"blank to select all options shown",
|
||||
force_interactive=True)
|
||||
|
||||
if code == OK:
|
||||
if len(ans.strip()) == 0:
|
||||
ans = " ".join(str(x) for x in range(1, len(tags)+1))
|
||||
indices = separate_list_input(ans)
|
||||
selected_tags = self._scrub_checklist_input(indices, tags)
|
||||
if selected_tags:
|
||||
|
|
@ -369,10 +239,67 @@ class FileDisplay(object):
|
|||
else:
|
||||
return code, []
|
||||
|
||||
def directory_select(self, message, **unused_kwargs):
|
||||
def _return_default(self, prompt, default, cli_flag, force_interactive):
|
||||
"""Should we return the default instead of prompting the user?
|
||||
|
||||
:param str prompt: prompt for the user
|
||||
:param default: default answer to prompt
|
||||
:param str cli_flag: command line option for setting an answer
|
||||
to this question
|
||||
:param bool force_interactive: if interactivity is forced by the
|
||||
IDisplay call
|
||||
|
||||
:returns: True if we should return the default without prompting
|
||||
:rtype: bool
|
||||
|
||||
"""
|
||||
# assert_valid_call(prompt, default, cli_flag, force_interactive)
|
||||
if self._can_interact(force_interactive):
|
||||
return False
|
||||
elif default is None:
|
||||
msg = "Unable to get an answer for the question:\n{0}".format(prompt)
|
||||
if cli_flag:
|
||||
msg += (
|
||||
"\nYou can provide an answer on the "
|
||||
"command line with the {0} flag.".format(cli_flag))
|
||||
raise errors.Error(msg)
|
||||
else:
|
||||
logger.debug(
|
||||
"Falling back to default %s for the prompt:\n%s",
|
||||
default, prompt)
|
||||
return True
|
||||
|
||||
def _can_interact(self, force_interactive):
|
||||
"""Can we safely interact with the user?
|
||||
|
||||
:param bool force_interactive: if interactivity is forced by the
|
||||
IDisplay call
|
||||
|
||||
:returns: True if the display can interact with the user
|
||||
:rtype: bool
|
||||
|
||||
"""
|
||||
if (self.force_interactive or force_interactive or
|
||||
sys.stdin.isatty() and self.outfile.isatty()):
|
||||
return True
|
||||
elif not self.skipped_interaction:
|
||||
logger.warning(
|
||||
"Skipped user interaction because Certbot doesn't appear to "
|
||||
"be running in a terminal. You should probably include "
|
||||
"--non-interactive or %s on the command line.",
|
||||
constants.FORCE_INTERACTIVE_FLAG)
|
||||
self.skipped_interaction = True
|
||||
return False
|
||||
|
||||
def directory_select(self, message, default=None, cli_flag=None,
|
||||
force_interactive=False, **unused_kwargs):
|
||||
"""Display a directory selection screen.
|
||||
|
||||
:param str message: prompt to give the user
|
||||
:param default: default value to return (if one exists)
|
||||
:param str cli_flag: option used to set this value with the CLI
|
||||
:param bool force_interactive: True if it's safe to prompt the user
|
||||
because it won't cause any workflow regressions
|
||||
|
||||
:returns: tuple of the form (`code`, `string`) where
|
||||
`code` - display exit code
|
||||
|
|
@ -380,7 +307,7 @@ class FileDisplay(object):
|
|||
|
||||
"""
|
||||
with completer.Completer():
|
||||
return self.input(message)
|
||||
return self.input(message, default, cli_flag, force_interactive)
|
||||
|
||||
def _scrub_checklist_input(self, indices, tags):
|
||||
# pylint: disable=no-self-use
|
||||
|
|
@ -478,11 +405,29 @@ class FileDisplay(object):
|
|||
return OK, selection
|
||||
|
||||
|
||||
def assert_valid_call(prompt, default, cli_flag, force_interactive):
|
||||
"""Verify that provided arguments is a valid IDisplay call.
|
||||
|
||||
:param str prompt: prompt for the user
|
||||
:param default: default answer to prompt
|
||||
:param str cli_flag: command line option for setting an answer
|
||||
to this question
|
||||
:param bool force_interactive: if interactivity is forced by the
|
||||
IDisplay call
|
||||
|
||||
"""
|
||||
msg = "Invalid IDisplay call for this prompt:\n{0}".format(prompt)
|
||||
if cli_flag:
|
||||
msg += ("\nYou can set an answer to "
|
||||
"this prompt with the {0} flag".format(cli_flag))
|
||||
assert default is not None or force_interactive, msg
|
||||
|
||||
|
||||
@zope.interface.implementer(interfaces.IDisplay)
|
||||
class NoninteractiveDisplay(object):
|
||||
"""An iDisplay implementation that never asks for interactive user input"""
|
||||
|
||||
def __init__(self, outfile):
|
||||
def __init__(self, outfile, *unused_args, **unused_kwargs):
|
||||
super(NoninteractiveDisplay, self).__init__()
|
||||
self.outfile = outfile
|
||||
|
||||
|
|
@ -496,23 +441,24 @@ class NoninteractiveDisplay(object):
|
|||
msg += "\n\n(You can set this with the {0} flag)".format(cli_flag)
|
||||
raise errors.MissingCommandlineFlag(msg)
|
||||
|
||||
def notification(self, message, height=10, pause=False):
|
||||
def notification(self, message, pause=False, wrap=True, **unused_kwargs):
|
||||
# pylint: disable=unused-argument
|
||||
"""Displays a notification without waiting for user acceptance.
|
||||
|
||||
:param str message: Message to display to stdout
|
||||
:param int height: No effect for NoninteractiveDisplay
|
||||
:param bool pause: The NoninteractiveDisplay waits for no keyboard
|
||||
:param bool wrap: Whether or not the application should wrap text
|
||||
|
||||
"""
|
||||
side_frame = "-" * 79
|
||||
message = _wrap_lines(message)
|
||||
if wrap:
|
||||
message = _wrap_lines(message)
|
||||
self.outfile.write(
|
||||
"{line}{frame}{line}{msg}{line}{frame}{line}".format(
|
||||
line=os.linesep, frame=side_frame, msg=message))
|
||||
|
||||
def menu(self, message, choices, ok_label=None, cancel_label=None,
|
||||
help_label=None, default=None, cli_flag=None):
|
||||
help_label=None, default=None, cli_flag=None, **unused_kwargs):
|
||||
# pylint: disable=unused-argument,too-many-arguments
|
||||
"""Avoid displaying a menu.
|
||||
|
||||
|
|
@ -535,7 +481,7 @@ class NoninteractiveDisplay(object):
|
|||
|
||||
return OK, default
|
||||
|
||||
def input(self, message, default=None, cli_flag=None):
|
||||
def input(self, message, default=None, cli_flag=None, **unused_kwargs):
|
||||
"""Accept input from the user.
|
||||
|
||||
:param str message: message to display to the user
|
||||
|
|
@ -552,7 +498,8 @@ class NoninteractiveDisplay(object):
|
|||
else:
|
||||
return OK, default
|
||||
|
||||
def yesno(self, message, yes_label=None, no_label=None, default=None, cli_flag=None):
|
||||
def yesno(self, message, yes_label=None, no_label=None,
|
||||
default=None, cli_flag=None, **unused_kwargs):
|
||||
# pylint: disable=unused-argument
|
||||
"""Decide Yes or No, without asking anybody
|
||||
|
||||
|
|
@ -569,8 +516,8 @@ class NoninteractiveDisplay(object):
|
|||
else:
|
||||
return default
|
||||
|
||||
def checklist(self, message, tags, default=None, cli_flag=None, **kwargs):
|
||||
# pylint: disable=unused-argument
|
||||
def checklist(self, message, tags, default=None,
|
||||
cli_flag=None, **unused_kwargs):
|
||||
"""Display a checklist.
|
||||
|
||||
:param str message: Message to display to user
|
||||
|
|
@ -588,7 +535,8 @@ class NoninteractiveDisplay(object):
|
|||
else:
|
||||
return OK, default
|
||||
|
||||
def directory_select(self, message, default=None, cli_flag=None):
|
||||
def directory_select(self, message, default=None,
|
||||
cli_flag=None, **unused_kwargs):
|
||||
"""Simulate prompting the user for a directory.
|
||||
|
||||
This function returns default if it is not ``None``, otherwise,
|
||||
|
|
|
|||
121
certbot/hooks.py
121
certbot/hooks.py
|
|
@ -7,21 +7,35 @@ import os
|
|||
from subprocess import Popen, PIPE
|
||||
|
||||
from certbot import errors
|
||||
from certbot import util
|
||||
|
||||
from certbot.plugins import util as plug_util
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def validate_hooks(config):
|
||||
"""Check hook commands are executable."""
|
||||
_validate_hook(config.pre_hook, "pre")
|
||||
_validate_hook(config.post_hook, "post")
|
||||
_validate_hook(config.renew_hook, "renew")
|
||||
validate_hook(config.pre_hook, "pre")
|
||||
validate_hook(config.post_hook, "post")
|
||||
validate_hook(config.renew_hook, "renew")
|
||||
|
||||
def _prog(shell_cmd):
|
||||
"""Extract the program run by a shell command"""
|
||||
cmd = _which(shell_cmd)
|
||||
return os.path.basename(cmd) if cmd else None
|
||||
"""Extract the program run by a shell command.
|
||||
|
||||
def _validate_hook(shell_cmd, hook_name):
|
||||
:param str shell_cmd: command to be executed
|
||||
|
||||
:returns: basename of command or None if the command isn't found
|
||||
:rtype: str or None
|
||||
|
||||
"""
|
||||
if not util.exe_exists(shell_cmd):
|
||||
plug_util.path_surgery(shell_cmd)
|
||||
if not util.exe_exists(shell_cmd):
|
||||
return None
|
||||
return os.path.basename(shell_cmd)
|
||||
|
||||
|
||||
def validate_hook(shell_cmd, hook_name):
|
||||
"""Check that a command provided as a hook is plausibly executable.
|
||||
|
||||
:raises .errors.HookCommandNotFound: if the command is not found
|
||||
|
|
@ -36,68 +50,79 @@ def _validate_hook(shell_cmd, hook_name):
|
|||
|
||||
def pre_hook(config):
|
||||
"Run pre-hook if it's defined and hasn't been run."
|
||||
if config.pre_hook and not pre_hook.already:
|
||||
logger.info("Running pre-hook command: %s", config.pre_hook)
|
||||
_run_hook(config.pre_hook)
|
||||
pre_hook.already = True
|
||||
cmd = config.pre_hook
|
||||
if cmd and cmd not in pre_hook.already:
|
||||
logger.info("Running pre-hook command: %s", cmd)
|
||||
_run_hook(cmd)
|
||||
pre_hook.already.add(cmd)
|
||||
elif cmd:
|
||||
logger.info("Pre-hook command already run, skipping: %s", cmd)
|
||||
|
||||
pre_hook.already = False
|
||||
pre_hook.already = set()
|
||||
|
||||
def post_hook(config, final=False):
|
||||
|
||||
def post_hook(config):
|
||||
"""Run post hook if defined.
|
||||
|
||||
If the verb is renew, we might have more certs to renew, so we wait until
|
||||
we're called with final=True before actually doing anything.
|
||||
run_saved_post_hooks() is called.
|
||||
"""
|
||||
if config.post_hook:
|
||||
if not pre_hook.already:
|
||||
logger.info("No renewals attempted, so not running post-hook")
|
||||
if config.verb != "renew":
|
||||
logger.warning("Sanity failure in renewal hooks")
|
||||
return
|
||||
if final or config.verb != "renew":
|
||||
logger.info("Running post-hook command: %s", config.post_hook)
|
||||
_run_hook(config.post_hook)
|
||||
|
||||
cmd = config.post_hook
|
||||
# In the "renew" case, we save these up to run at the end
|
||||
if config.verb == "renew":
|
||||
if cmd and cmd not in post_hook.eventually:
|
||||
post_hook.eventually.append(cmd)
|
||||
# certonly / run
|
||||
elif cmd:
|
||||
logger.info("Running post-hook command: %s", cmd)
|
||||
_run_hook(cmd)
|
||||
|
||||
post_hook.eventually = []
|
||||
|
||||
def run_saved_post_hooks():
|
||||
"""Run any post hooks that were saved up in the course of the 'renew' verb"""
|
||||
for cmd in post_hook.eventually:
|
||||
logger.info("Running post-hook command: %s", cmd)
|
||||
_run_hook(cmd)
|
||||
|
||||
|
||||
def renew_hook(config, domains, lineage_path):
|
||||
"Run post-renewal hook if defined."
|
||||
"""Run post-renewal hook if defined."""
|
||||
if config.renew_hook:
|
||||
if not config.dry_run:
|
||||
os.environ["RENEWED_DOMAINS"] = " ".join(domains)
|
||||
os.environ["RENEWED_LINEAGE"] = lineage_path
|
||||
logger.info("Running renew-hook command: %s", config.renew_hook)
|
||||
_run_hook(config.renew_hook)
|
||||
else:
|
||||
logger.warning("Dry run: skipping renewal hook command: %s", config.renew_hook)
|
||||
|
||||
|
||||
def _run_hook(shell_cmd):
|
||||
"""Run a hook command.
|
||||
|
||||
:returns: stderr if there was any"""
|
||||
|
||||
cmd = Popen(shell_cmd, shell=True, stdout=PIPE, stderr=PIPE, stdin=PIPE)
|
||||
_out, err = cmd.communicate()
|
||||
err, _ = execute(shell_cmd)
|
||||
return err
|
||||
|
||||
|
||||
def execute(shell_cmd):
|
||||
"""Run a command.
|
||||
|
||||
:returns: `tuple` (`str` stderr, `str` stdout)"""
|
||||
|
||||
# universal_newlines causes Popen.communicate()
|
||||
# to return str objects instead of bytes in Python 3
|
||||
cmd = Popen(shell_cmd, shell=True, stdout=PIPE,
|
||||
stderr=PIPE, universal_newlines=True)
|
||||
out, err = cmd.communicate()
|
||||
if cmd.returncode != 0:
|
||||
logger.error('Hook command "%s" returned error code %d', shell_cmd, cmd.returncode)
|
||||
logger.error('Hook command "%s" returned error code %d',
|
||||
shell_cmd, cmd.returncode)
|
||||
if err:
|
||||
logger.error('Error output from %s:\n%s', _prog(shell_cmd), err)
|
||||
base_cmd = os.path.basename(shell_cmd.split(None, 1)[0])
|
||||
logger.error('Error output from %s:\n%s', base_cmd, err)
|
||||
return (err, out)
|
||||
|
||||
def _is_exe(fpath):
|
||||
return os.path.isfile(fpath) and os.access(fpath, os.X_OK)
|
||||
|
||||
def _which(program):
|
||||
"""Test if program is in the path."""
|
||||
# Borrowed from:
|
||||
# https://stackoverflow.com/questions/377017/test-if-executable-exists-in-python
|
||||
# XXX May need more porting to handle .exe extensions on Windows
|
||||
|
||||
fpath, _fname = os.path.split(program)
|
||||
if fpath:
|
||||
if _is_exe(program):
|
||||
return program
|
||||
else:
|
||||
for path in os.environ["PATH"].split(os.pathsep):
|
||||
exe_file = os.path.join(path, program)
|
||||
if _is_exe(exe_file):
|
||||
return exe_file
|
||||
|
||||
return None
|
||||
|
|
|
|||
|
|
@ -138,15 +138,15 @@ class IAuthenticator(IPlugin):
|
|||
"""
|
||||
|
||||
def get_chall_pref(domain):
|
||||
"""Return list of challenge preferences.
|
||||
"""Return `collections.Iterable` of challenge preferences.
|
||||
|
||||
:param str domain: Domain for which challenge preferences are sought.
|
||||
|
||||
:returns: List of challenge types (subclasses of
|
||||
:returns: `collections.Iterable` of challenge types (subclasses of
|
||||
:class:`acme.challenges.Challenge`) with the most
|
||||
preferred challenges first. If a type is not specified, it means the
|
||||
Authenticator cannot perform the challenge.
|
||||
:rtype: list
|
||||
:rtype: `collections.Iterable`
|
||||
|
||||
"""
|
||||
|
||||
|
|
@ -158,7 +158,7 @@ class IAuthenticator(IPlugin):
|
|||
instances, such that it contains types found within
|
||||
:func:`get_chall_pref` only.
|
||||
|
||||
:returns: List of ACME
|
||||
:returns: `collections.Iterable` of ACME
|
||||
:class:`~acme.challenges.ChallengeResponse` instances
|
||||
or if the :class:`~acme.challenges.Challenge` cannot
|
||||
be fulfilled then:
|
||||
|
|
@ -168,7 +168,7 @@ class IAuthenticator(IPlugin):
|
|||
``False``
|
||||
Authenticator will never be able to perform (error).
|
||||
|
||||
:rtype: :class:`list` of
|
||||
:rtype: :class:`collections.Iterable` of
|
||||
:class:`acme.challenges.ChallengeResponse`,
|
||||
where responses are required to be returned in
|
||||
the same order as corresponding input challenges
|
||||
|
|
@ -201,7 +201,7 @@ class IConfig(zope.interface.Interface):
|
|||
"""
|
||||
server = zope.interface.Attribute("ACME Directory Resource URI.")
|
||||
email = zope.interface.Attribute(
|
||||
"Email used for registration and recovery contact.")
|
||||
"Email used for registration and recovery contact. (default: Ask)")
|
||||
rsa_key_size = zope.interface.Attribute("Size of the RSA key.")
|
||||
must_staple = zope.interface.Attribute(
|
||||
"Adds the OCSP Must Staple extension to the certificate. "
|
||||
|
|
@ -223,9 +223,6 @@ class IConfig(zope.interface.Interface):
|
|||
temp_checkpoint_dir = zope.interface.Attribute(
|
||||
"Temporary checkpoint directory.")
|
||||
|
||||
renewer_config_file = zope.interface.Attribute(
|
||||
"Location of renewal configuration file.")
|
||||
|
||||
no_verify_ssl = zope.interface.Attribute(
|
||||
"Disable verification of the ACME server's certificate.")
|
||||
tls_sni_01_port = zope.interface.Attribute(
|
||||
|
|
@ -234,7 +231,7 @@ class IConfig(zope.interface.Interface):
|
|||
"A conforming ACME server will still attempt to connect on port 443.")
|
||||
|
||||
http01_port = zope.interface.Attribute(
|
||||
"Port used in the http-01 challenge."
|
||||
"Port used in the http-01 challenge. "
|
||||
"This only affects the port Certbot listens on. "
|
||||
"A conforming ACME server will still attempt to connect on port 80.")
|
||||
|
||||
|
|
@ -257,7 +254,7 @@ class IInstaller(IPlugin):
|
|||
def get_all_names():
|
||||
"""Returns all names that may be authenticated.
|
||||
|
||||
:rtype: `list` of `str`
|
||||
:rtype: `collections.Iterable` of `str`
|
||||
|
||||
"""
|
||||
|
||||
|
|
@ -292,24 +289,11 @@ class IInstaller(IPlugin):
|
|||
"""
|
||||
|
||||
def supported_enhancements():
|
||||
"""Returns a list of supported enhancements.
|
||||
"""Returns a `collections.Iterable` of supported enhancements.
|
||||
|
||||
:returns: supported enhancements which should be a subset of
|
||||
:const:`~certbot.constants.ENHANCEMENTS`
|
||||
:rtype: :class:`list` of :class:`str`
|
||||
|
||||
"""
|
||||
|
||||
def get_all_certs_keys():
|
||||
"""Retrieve all certs and keys set in configuration.
|
||||
|
||||
:returns: tuples with form `[(cert, key, path)]`, where:
|
||||
|
||||
- `cert` - str path to certificate file
|
||||
- `key` - str path to associated key file
|
||||
- `path` - file path to configuration file
|
||||
|
||||
:rtype: list
|
||||
:rtype: :class:`collections.Iterable` of :class:`str`
|
||||
|
||||
"""
|
||||
|
||||
|
|
@ -377,21 +361,29 @@ class IInstaller(IPlugin):
|
|||
|
||||
class IDisplay(zope.interface.Interface):
|
||||
"""Generic display."""
|
||||
# pylint: disable=too-many-arguments
|
||||
# see https://github.com/certbot/certbot/issues/3915
|
||||
|
||||
def notification(message, height, pause):
|
||||
def notification(message, pause, wrap=True, force_interactive=False):
|
||||
"""Displays a string message
|
||||
|
||||
:param str message: Message to display
|
||||
:param int height: Height of dialog box if applicable
|
||||
:param bool pause: Whether or not the application should pause for
|
||||
confirmation (if available)
|
||||
:param bool wrap: Whether or not the application should wrap text
|
||||
:param bool force_interactive: True if it's safe to prompt the user
|
||||
because it won't cause any workflow regressions
|
||||
|
||||
"""
|
||||
|
||||
def menu(message, choices, ok_label="OK", # pylint: disable=too-many-arguments
|
||||
cancel_label="Cancel", help_label="", default=None, cli_flag=None):
|
||||
def menu(message, choices, ok_label="OK",
|
||||
cancel_label="Cancel", help_label="",
|
||||
default=None, cli_flag=None, force_interactive=False):
|
||||
"""Displays a generic menu.
|
||||
|
||||
When not setting force_interactive=True, you must provide a
|
||||
default value.
|
||||
|
||||
:param str message: message to display
|
||||
|
||||
:param choices: choices
|
||||
|
|
@ -402,6 +394,8 @@ class IDisplay(zope.interface.Interface):
|
|||
:param str help_label: label for Help button
|
||||
:param int default: default (non-interactive) choice from the menu
|
||||
:param str cli_flag: to automate choice from the menu, eg "--keep"
|
||||
:param bool force_interactive: True if it's safe to prompt the user
|
||||
because it won't cause any workflow regressions
|
||||
|
||||
:returns: tuple of (`code`, `index`) where
|
||||
`code` - str display exit code
|
||||
|
|
@ -412,10 +406,16 @@ class IDisplay(zope.interface.Interface):
|
|||
|
||||
"""
|
||||
|
||||
def input(message, default=None, cli_args=None):
|
||||
def input(message, default=None, cli_args=None, force_interactive=False):
|
||||
"""Accept input from the user.
|
||||
|
||||
When not setting force_interactive=True, you must provide a
|
||||
default value.
|
||||
|
||||
:param str message: message to display to the user
|
||||
:param str default: default (non-interactive) response to prompt
|
||||
:param bool force_interactive: True if it's safe to prompt the user
|
||||
because it won't cause any workflow regressions
|
||||
|
||||
:returns: tuple of (`code`, `input`) where
|
||||
`code` - str display exit code
|
||||
|
|
@ -428,14 +428,19 @@ class IDisplay(zope.interface.Interface):
|
|||
"""
|
||||
|
||||
def yesno(message, yes_label="Yes", no_label="No", default=None,
|
||||
cli_args=None):
|
||||
cli_args=None, force_interactive=False):
|
||||
"""Query the user with a yes/no question.
|
||||
|
||||
Yes and No label must begin with different letters.
|
||||
|
||||
When not setting force_interactive=True, you must provide a
|
||||
default value.
|
||||
|
||||
:param str message: question for the user
|
||||
:param str default: default (non-interactive) choice from the menu
|
||||
:param str cli_flag: to automate choice from the menu, eg "--redirect / --no-redirect"
|
||||
:param bool force_interactive: True if it's safe to prompt the user
|
||||
because it won't cause any workflow regressions
|
||||
|
||||
:returns: True for "Yes", False for "No"
|
||||
:rtype: bool
|
||||
|
|
@ -445,14 +450,20 @@ class IDisplay(zope.interface.Interface):
|
|||
|
||||
"""
|
||||
|
||||
def checklist(message, tags, default_state, default=None, cli_args=None):
|
||||
def checklist(message, tags, default_state,
|
||||
default=None, cli_args=None, force_interactive=False):
|
||||
"""Allow for multiple selections from a menu.
|
||||
|
||||
When not setting force_interactive=True, you must provide a
|
||||
default value.
|
||||
|
||||
:param str message: message to display to the user
|
||||
:param list tags: where each is of type :class:`str` len(tags) > 0
|
||||
:param bool default_status: If True, items are in a selected state by default.
|
||||
:param str default: default (non-interactive) state of the checklist
|
||||
:param str cli_flag: to automate choice from the menu, eg "--domains"
|
||||
:param bool force_interactive: True if it's safe to prompt the user
|
||||
because it won't cause any workflow regressions
|
||||
|
||||
:returns: tuple of the form (code, list_tags) where
|
||||
`code` - int display exit code
|
||||
|
|
@ -464,15 +475,21 @@ class IDisplay(zope.interface.Interface):
|
|||
|
||||
"""
|
||||
|
||||
def directory_select(self, message, default=None, cli_flag=None):
|
||||
def directory_select(self, message, default=None,
|
||||
cli_flag=None, force_interactive=False):
|
||||
"""Display a directory selection screen.
|
||||
|
||||
When not setting force_interactive=True, you must provide a
|
||||
default value.
|
||||
|
||||
:param str message: prompt to give the user
|
||||
:param default: the default value to return, if one exists, when
|
||||
using the NoninteractiveDisplay
|
||||
:param str cli_flag: option used to set this value with the CLI,
|
||||
if one exists, to be included in error messages given by
|
||||
NoninteractiveDisplay
|
||||
:param bool force_interactive: True if it's safe to prompt the user
|
||||
because it won't cause any workflow regressions
|
||||
|
||||
:returns: tuple of the form (`code`, `string`) where
|
||||
`code` - int display exit code
|
||||
|
|
|
|||
|
|
@ -1,64 +0,0 @@
|
|||
"""Logging utilities."""
|
||||
import logging
|
||||
|
||||
import dialog
|
||||
|
||||
from certbot.display import util as display_util
|
||||
|
||||
|
||||
class DialogHandler(logging.Handler): # pylint: disable=too-few-public-methods
|
||||
"""Logging handler using dialog info box.
|
||||
|
||||
:ivar int height: Height of the info box (without padding).
|
||||
:ivar int width: Width of the info box (without padding).
|
||||
:ivar list lines: Lines to be displayed in the info box.
|
||||
:ivar d: Instance of :class:`dialog.Dialog`.
|
||||
|
||||
"""
|
||||
|
||||
PADDING_HEIGHT = 2
|
||||
PADDING_WIDTH = 4
|
||||
|
||||
def __init__(self, level=logging.NOTSET, height=display_util.HEIGHT,
|
||||
width=display_util.WIDTH - 4, d=None):
|
||||
# Handler not new-style -> no super
|
||||
logging.Handler.__init__(self, level)
|
||||
self.height = height
|
||||
self.width = width
|
||||
# "dialog" collides with module name...
|
||||
self.d = dialog.Dialog() if d is None else d
|
||||
self.lines = []
|
||||
|
||||
def emit(self, record):
|
||||
"""Emit message to a dialog info box.
|
||||
|
||||
Only show the last (self.height) lines; note that lines can wrap
|
||||
at self.width, so a single line could actually be multiple
|
||||
lines.
|
||||
|
||||
"""
|
||||
for line in self.format(record).splitlines():
|
||||
# check for lines that would wrap
|
||||
cur_out = line
|
||||
while len(cur_out) > self.width:
|
||||
# find first space before self.width chars into cur_out
|
||||
last_space_pos = cur_out.rfind(' ', 0, self.width)
|
||||
|
||||
if last_space_pos == -1:
|
||||
# no spaces, just cut them off at whatever
|
||||
self.lines.append(cur_out[0:self.width])
|
||||
cur_out = cur_out[self.width:]
|
||||
else:
|
||||
# cut off at last space
|
||||
self.lines.append(cur_out[0:last_space_pos])
|
||||
cur_out = cur_out[last_space_pos + 1:]
|
||||
if cur_out != '':
|
||||
self.lines.append(cur_out)
|
||||
|
||||
# show last 16 lines
|
||||
content = '\n'.join(self.lines[-self.height:])
|
||||
|
||||
# add the padding around the box
|
||||
self.d.infobox(
|
||||
content, self.height + self.PADDING_HEIGHT,
|
||||
self.width + self.PADDING_WIDTH)
|
||||
303
certbot/main.py
303
certbot/main.py
|
|
@ -1,7 +1,6 @@
|
|||
"""Certbot main entry point."""
|
||||
from __future__ import print_function
|
||||
import atexit
|
||||
import dialog
|
||||
import functools
|
||||
import logging.handlers
|
||||
import os
|
||||
|
|
@ -13,10 +12,12 @@ import zope.component
|
|||
|
||||
from acme import jose
|
||||
from acme import messages
|
||||
from acme import errors as acme_errors
|
||||
|
||||
import certbot
|
||||
|
||||
from certbot import account
|
||||
from certbot import cert_manager
|
||||
from certbot import client
|
||||
from certbot import cli
|
||||
from certbot import crypto_util
|
||||
|
|
@ -27,10 +28,8 @@ from certbot import errors
|
|||
from certbot import hooks
|
||||
from certbot import interfaces
|
||||
from certbot import util
|
||||
from certbot import log
|
||||
from certbot import reporter
|
||||
from certbot import renewal
|
||||
from certbot import storage
|
||||
|
||||
from certbot.display import util as display_util, ops as display_ops
|
||||
from certbot.plugins import disco as plugins_disco
|
||||
|
|
@ -40,7 +39,10 @@ from certbot.plugins import selection as plug_sel
|
|||
_PERM_ERR_FMT = os.linesep.join((
|
||||
"The following error was encountered:", "{0}",
|
||||
"If running as non-root, set --config-dir, "
|
||||
"--logs-dir, and --work-dir to writeable paths."))
|
||||
"--work-dir, and --logs-dir to writeable paths."))
|
||||
|
||||
USER_CANCELLED = ("User chose to cancel the operation and may "
|
||||
"reinvoke the client.")
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
|
@ -68,17 +70,21 @@ def _report_successful_dry_run(config):
|
|||
reporter_util.HIGH_PRIORITY, on_crash=False)
|
||||
|
||||
|
||||
def _auth_from_domains(le_client, config, domains, lineage=None):
|
||||
def _auth_from_available(le_client, config, domains=None, certname=None, lineage=None):
|
||||
"""Authenticate and enroll certificate.
|
||||
|
||||
:returns: Tuple of (str action, cert_or_None) as per _treat_as_renewal
|
||||
This method finds the relevant lineage, figures out what to do with it,
|
||||
then performs that action. Includes calls to hooks, various reports,
|
||||
checks, and requests for user input.
|
||||
|
||||
:returns: Tuple of (str action, cert_or_None) as per _find_lineage_for_domains_and_certname
|
||||
action can be: "newcert" | "renew" | "reinstall"
|
||||
"""
|
||||
# If lineage is specified, use that one instead of looking around for
|
||||
# a matching one.
|
||||
if lineage is None:
|
||||
# This will find a relevant matching lineage that exists
|
||||
action, lineage = _treat_as_renewal(config, domains)
|
||||
action, lineage = _find_lineage_for_domains_and_certname(config, domains, certname)
|
||||
else:
|
||||
# Renewal, where we already know the specific lineage we're
|
||||
# interested in
|
||||
|
|
@ -94,15 +100,15 @@ def _auth_from_domains(le_client, config, domains, lineage=None):
|
|||
try:
|
||||
if action == "renew":
|
||||
logger.info("Renewing an existing certificate")
|
||||
renewal.renew_cert(config, domains, le_client, lineage)
|
||||
renewal.renew_cert(config, le_client, lineage)
|
||||
elif action == "newcert":
|
||||
# TREAT AS NEW REQUEST
|
||||
logger.info("Obtaining a new certificate")
|
||||
lineage = le_client.obtain_and_enroll_certificate(domains)
|
||||
lineage = le_client.obtain_and_enroll_certificate(domains, certname)
|
||||
if lineage is False:
|
||||
raise errors.Error("Certificate could not be obtained")
|
||||
finally:
|
||||
hooks.post_hook(config, final=False)
|
||||
hooks.post_hook(config)
|
||||
|
||||
if not config.dry_run and not config.verb == "renew":
|
||||
_report_new_cert(config, lineage.cert, lineage.fullchain)
|
||||
|
|
@ -115,7 +121,7 @@ def _handle_subset_cert_request(config, domains, cert):
|
|||
|
||||
:param storage.RenewableCert cert:
|
||||
|
||||
:returns: Tuple of (str action, cert_or_None) as per _treat_as_renewal
|
||||
:returns: Tuple of (str action, cert_or_None) as per _find_lineage_for_domains_and_certname
|
||||
action can be: "newcert" | "renew" | "reinstall"
|
||||
:rtype: tuple
|
||||
|
||||
|
|
@ -133,7 +139,8 @@ def _handle_subset_cert_request(config, domains, cert):
|
|||
br=os.linesep)
|
||||
if config.expand or config.renew_by_default or zope.component.getUtility(
|
||||
interfaces.IDisplay).yesno(question, "Expand", "Cancel",
|
||||
cli_flag="--expand"):
|
||||
cli_flag="--expand",
|
||||
force_interactive=True):
|
||||
return "renew", cert
|
||||
else:
|
||||
reporter_util = zope.component.getUtility(interfaces.IReporter)
|
||||
|
|
@ -147,9 +154,7 @@ def _handle_subset_cert_request(config, domains, cert):
|
|||
br=os.linesep
|
||||
),
|
||||
reporter_util.HIGH_PRIORITY)
|
||||
raise errors.Error(
|
||||
"User chose to cancel the operation and may "
|
||||
"reinvoke the client.")
|
||||
raise errors.Error(USER_CANCELLED)
|
||||
|
||||
|
||||
def _handle_identical_cert_request(config, lineage):
|
||||
|
|
@ -157,7 +162,7 @@ def _handle_identical_cert_request(config, lineage):
|
|||
|
||||
:param storage.RenewableCert lineage:
|
||||
|
||||
:returns: Tuple of (str action, cert_or_None) as per _treat_as_renewal
|
||||
:returns: Tuple of (str action, cert_or_None) as per _find_lineage_for_domains_and_certname
|
||||
action can be: "newcert" | "renew" | "reinstall"
|
||||
:rtype: tuple
|
||||
|
||||
|
|
@ -171,8 +176,8 @@ def _handle_identical_cert_request(config, lineage):
|
|||
# reinstalled without further prompting.
|
||||
return "reinstall", lineage
|
||||
question = (
|
||||
"You have an existing certificate that contains exactly the same "
|
||||
"domains you requested and isn't close to expiry."
|
||||
"You have an existing certificate that has exactly the same "
|
||||
"domains or certificate name you requested and isn't close to expiry."
|
||||
"{br}(ref: {0}){br}{br}What would you like to do?"
|
||||
).format(lineage.configfile.filename, br=os.linesep)
|
||||
|
||||
|
|
@ -184,7 +189,8 @@ def _handle_identical_cert_request(config, lineage):
|
|||
"Renew & replace the cert (limit ~5 per 7 days)"]
|
||||
|
||||
display = zope.component.getUtility(interfaces.IDisplay)
|
||||
response = display.menu(question, choices, "OK", "Cancel", default=0)
|
||||
response = display.menu(question, choices, "OK", "Cancel",
|
||||
default=0, force_interactive=True)
|
||||
if response[0] == display_util.CANCEL:
|
||||
# TODO: Add notification related to command-line options for
|
||||
# skipping the menu for this case.
|
||||
|
|
@ -198,8 +204,7 @@ def _handle_identical_cert_request(config, lineage):
|
|||
else:
|
||||
assert False, "This is impossible"
|
||||
|
||||
|
||||
def _treat_as_renewal(config, domains):
|
||||
def _find_lineage_for_domains(config, domains):
|
||||
"""Determine whether there are duplicated names and how to handle
|
||||
them (renew, reinstall, newcert, or raising an error to stop
|
||||
the client run if the user chooses to cancel the operation when
|
||||
|
|
@ -219,7 +224,7 @@ def _treat_as_renewal(config, domains):
|
|||
if config.duplicate:
|
||||
return "newcert", None
|
||||
# TODO: Also address superset case
|
||||
ident_names_cert, subset_names_cert = _find_duplicative_certs(config, domains)
|
||||
ident_names_cert, subset_names_cert = cert_manager.find_duplicative_certs(config, domains)
|
||||
# XXX ^ schoen is not sure whether that correctly reads the systemwide
|
||||
# configuration file.
|
||||
if ident_names_cert is None and subset_names_cert is None:
|
||||
|
|
@ -230,51 +235,75 @@ def _treat_as_renewal(config, domains):
|
|||
elif subset_names_cert is not None:
|
||||
return _handle_subset_cert_request(config, domains, subset_names_cert)
|
||||
|
||||
def _find_lineage_for_domains_and_certname(config, domains, certname):
|
||||
"""Find appropriate lineage based on given domains and/or certname.
|
||||
|
||||
def _find_duplicative_certs(config, domains):
|
||||
"""Find existing certs that duplicate the request."""
|
||||
:returns: Two-element tuple containing desired new-certificate behavior as
|
||||
a string token ("reinstall", "renew", or "newcert"), plus either
|
||||
a RenewableCert instance or None if renewal shouldn't occur.
|
||||
|
||||
identical_names_cert, subset_names_cert = None, None
|
||||
:raises .Error: If the user would like to rerun the client again.
|
||||
|
||||
cli_config = configuration.RenewerConfiguration(config)
|
||||
configs_dir = cli_config.renewal_configs_dir
|
||||
# Verify the directory is there
|
||||
util.make_or_verify_dir(configs_dir, mode=0o755, uid=os.geteuid())
|
||||
"""
|
||||
if not certname:
|
||||
return _find_lineage_for_domains(config, domains)
|
||||
else:
|
||||
lineage = cert_manager.lineage_for_certname(config, certname)
|
||||
if lineage:
|
||||
if domains:
|
||||
if set(cert_manager.domains_for_certname(config, certname)) != set(domains):
|
||||
_ask_user_to_confirm_new_names(config, domains, certname,
|
||||
lineage.names()) # raises if no
|
||||
return "renew", lineage
|
||||
# unnecessarily specified domains or no domains specified
|
||||
return _handle_identical_cert_request(config, lineage)
|
||||
else:
|
||||
if domains:
|
||||
return "newcert", None
|
||||
else:
|
||||
raise errors.ConfigurationError("No certificate with name {0} found. "
|
||||
"Use -d to specify domains, or run certbot --certificates to see "
|
||||
"possible certificate names.".format(certname))
|
||||
|
||||
for renewal_file in renewal.renewal_conf_files(cli_config):
|
||||
try:
|
||||
candidate_lineage = storage.RenewableCert(renewal_file, cli_config)
|
||||
except (errors.CertStorageError, IOError):
|
||||
logger.warning("Renewal conf file %s is broken. Skipping.", renewal_file)
|
||||
logger.debug("Traceback was:\n%s", traceback.format_exc())
|
||||
continue
|
||||
# TODO: Handle these differently depending on whether they are
|
||||
# expired or still valid?
|
||||
candidate_names = set(candidate_lineage.names())
|
||||
if candidate_names == set(domains):
|
||||
identical_names_cert = candidate_lineage
|
||||
elif candidate_names.issubset(set(domains)):
|
||||
# This logic finds and returns the largest subset-names cert
|
||||
# in the case where there are several available.
|
||||
if subset_names_cert is None:
|
||||
subset_names_cert = candidate_lineage
|
||||
elif len(candidate_names) > len(subset_names_cert.names()):
|
||||
subset_names_cert = candidate_lineage
|
||||
def _ask_user_to_confirm_new_names(config, new_domains, certname, old_domains):
|
||||
"""Ask user to confirm update cert certname to contain new_domains.
|
||||
"""
|
||||
if config.renew_with_new_domains:
|
||||
return
|
||||
msg = ("Confirm that you intend to update certificate {0} "
|
||||
"to include domains {1}. Note that it previously "
|
||||
"contained domains {2}.".format(
|
||||
certname,
|
||||
new_domains,
|
||||
old_domains))
|
||||
obj = zope.component.getUtility(interfaces.IDisplay)
|
||||
if not obj.yesno(msg, "Update cert", "Cancel", default=True):
|
||||
raise errors.ConfigurationError("Specified mismatched cert name and domains.")
|
||||
|
||||
return identical_names_cert, subset_names_cert
|
||||
|
||||
|
||||
def _find_domains(config, installer):
|
||||
def _find_domains_or_certname(config, installer):
|
||||
"""Retrieve domains and certname from config or user input.
|
||||
"""
|
||||
domains = None
|
||||
certname = config.certname
|
||||
# first, try to get domains from the config
|
||||
if config.domains:
|
||||
domains = config.domains
|
||||
else:
|
||||
# if we can't do that but we have a certname, get the domains
|
||||
# with that certname
|
||||
elif certname:
|
||||
domains = cert_manager.domains_for_certname(config, certname)
|
||||
|
||||
# that certname might not have existed, or there was a problem.
|
||||
# try to get domains from the user.
|
||||
if not domains:
|
||||
domains = display_ops.choose_names(installer)
|
||||
|
||||
if not domains:
|
||||
if not domains and not certname:
|
||||
raise errors.Error("Please specify --domains, or --installer that "
|
||||
"will help in domain names autodiscovery")
|
||||
"will help in domain names autodiscovery, or "
|
||||
"--cert-name for an existing certificate name.")
|
||||
|
||||
return domains
|
||||
return domains, certname
|
||||
|
||||
|
||||
def _report_new_cert(config, cert_path, fullchain_path):
|
||||
|
|
@ -347,7 +376,8 @@ def _determine_account(config):
|
|||
"server at {1}".format(
|
||||
regr.terms_of_service, config.server))
|
||||
obj = zope.component.getUtility(interfaces.IDisplay)
|
||||
return obj.yesno(msg, "Agree", "Cancel", cli_flag="--agree-tos")
|
||||
return obj.yesno(msg, "Agree", "Cancel",
|
||||
cli_flag="--agree-tos", force_interactive=True)
|
||||
|
||||
try:
|
||||
acc, acme = client.register(
|
||||
|
|
@ -429,13 +459,13 @@ def install(config, plugins):
|
|||
except errors.PluginSelectionError as e:
|
||||
return e.message
|
||||
|
||||
domains = _find_domains(config, installer)
|
||||
domains, _ = _find_domains_or_certname(config, installer)
|
||||
le_client = _init_le_client(config, authenticator=None, installer=installer)
|
||||
assert config.cert_path is not None # required=True in the subparser
|
||||
le_client.deploy_certificate(
|
||||
domains, config.key_path, config.cert_path, config.chain_path,
|
||||
config.fullchain_path)
|
||||
le_client.enhance_config(domains, config, config.chain_path)
|
||||
le_client.enhance_config(domains, config.chain_path)
|
||||
|
||||
|
||||
def plugins_cmd(config, plugins): # TODO: Use IDisplay rather than print
|
||||
|
|
@ -477,6 +507,34 @@ def config_changes(config, unused_plugins):
|
|||
"""
|
||||
client.view_config_changes(config, num=config.num)
|
||||
|
||||
def update_symlinks(config, unused_plugins):
|
||||
"""Update the certificate file family symlinks
|
||||
|
||||
Use the information in the config file to make symlinks point to
|
||||
the correct archive directory.
|
||||
"""
|
||||
cert_manager.update_live_symlinks(config)
|
||||
|
||||
def rename(config, unused_plugins):
|
||||
"""Rename a certificate
|
||||
|
||||
Use the information in the config file to rename an existing
|
||||
lineage.
|
||||
"""
|
||||
cert_manager.rename_lineage(config)
|
||||
|
||||
def delete(config, unused_plugins):
|
||||
"""Delete a certificate
|
||||
|
||||
Use the information in the config file to delete an existing
|
||||
lineage.
|
||||
"""
|
||||
cert_manager.delete(config)
|
||||
|
||||
def certificates(config, unused_plugins):
|
||||
"""Display information about certs configured with Certbot
|
||||
"""
|
||||
cert_manager.certificates(config)
|
||||
|
||||
def revoke(config, unused_plugins): # TODO: coop with renewal config
|
||||
"""Revoke a previously obtained certificate."""
|
||||
|
|
@ -492,7 +550,14 @@ def revoke(config, unused_plugins): # TODO: coop with renewal config
|
|||
key = acc.key
|
||||
acme = client.acme_from_config_key(config, key)
|
||||
cert = crypto_util.pyopenssl_load_certificate(config.cert_path[1])[0]
|
||||
acme.revoke(jose.ComparableX509(cert))
|
||||
logger.debug("Reason code for revocation: %s", config.reason)
|
||||
|
||||
try:
|
||||
acme.revoke(jose.ComparableX509(cert), config.reason)
|
||||
except acme_errors.ClientError as e:
|
||||
return e.message
|
||||
|
||||
display_ops.success_revocation(config.cert_path[0])
|
||||
|
||||
|
||||
def run(config, plugins): # pylint: disable=too-many-branches,too-many-locals
|
||||
|
|
@ -504,23 +569,23 @@ def run(config, plugins): # pylint: disable=too-many-branches,too-many-locals
|
|||
except errors.PluginSelectionError as e:
|
||||
return e.message
|
||||
|
||||
domains = _find_domains(config, installer)
|
||||
domains, certname = _find_domains_or_certname(config, installer)
|
||||
|
||||
# TODO: Handle errors from _init_le_client?
|
||||
le_client = _init_le_client(config, authenticator, installer)
|
||||
|
||||
action, lineage = _auth_from_domains(le_client, config, domains)
|
||||
action, lineage = _auth_from_available(le_client, config, domains, certname)
|
||||
|
||||
le_client.deploy_certificate(
|
||||
domains, lineage.privkey, lineage.cert,
|
||||
lineage.chain, lineage.fullchain)
|
||||
|
||||
le_client.enhance_config(domains, config, lineage.chain)
|
||||
le_client.enhance_config(domains, lineage.chain)
|
||||
|
||||
if len(lineage.available_versions("cert")) == 1:
|
||||
if action in ("newcert", "reinstall",):
|
||||
display_ops.success_installation(domains)
|
||||
else:
|
||||
display_ops.success_renewal(domains, action)
|
||||
display_ops.success_renewal(domains)
|
||||
|
||||
_suggest_donation_if_appropriate(config, action)
|
||||
|
||||
|
|
@ -542,7 +607,6 @@ def _csr_obtain_cert(config, le_client):
|
|||
certr, chain, config.cert_path, config.chain_path, config.fullchain_path)
|
||||
_report_new_cert(config, cert_path, cert_fullchain)
|
||||
|
||||
|
||||
def obtain_cert(config, plugins, lineage=None):
|
||||
"""Authenticate & obtain cert, but do not install it.
|
||||
|
||||
|
|
@ -560,8 +624,8 @@ def obtain_cert(config, plugins, lineage=None):
|
|||
|
||||
# SHOWTIME: Possibly obtain/renew a cert, and set action to renew | newcert | reinstall
|
||||
if config.csr is None: # the common case
|
||||
domains = _find_domains(config, installer)
|
||||
action, _ = _auth_from_domains(le_client, config, domains, lineage)
|
||||
domains, certname = _find_domains_or_certname(config, installer)
|
||||
action, _ = _auth_from_available(le_client, config, domains, certname, lineage)
|
||||
else:
|
||||
assert lineage is None, "Did not expect a CSR with a RenewableCert"
|
||||
_csr_obtain_cert(config, le_client)
|
||||
|
|
@ -590,9 +654,9 @@ def obtain_cert(config, plugins, lineage=None):
|
|||
def renew(config, unused_plugins):
|
||||
"""Renew previously-obtained certificates."""
|
||||
try:
|
||||
renewal.renew_all_lineages(config)
|
||||
renewal.handle_renewal_request(config)
|
||||
finally:
|
||||
hooks.post_hook(config, final=True)
|
||||
hooks.run_saved_post_hooks()
|
||||
|
||||
|
||||
def setup_log_file_handler(config, logfile, fmt):
|
||||
|
|
@ -614,14 +678,9 @@ def setup_log_file_handler(config, logfile, fmt):
|
|||
return handler, log_file_path
|
||||
|
||||
|
||||
def _cli_log_handler(config, level, fmt):
|
||||
if config.text_mode or config.noninteractive_mode or config.verb == "renew":
|
||||
handler = colored_logging.StreamHandler()
|
||||
handler.setFormatter(logging.Formatter(fmt))
|
||||
else:
|
||||
handler = log.DialogHandler()
|
||||
# dialog box is small, display as less as possible
|
||||
handler.setFormatter(logging.Formatter("%(message)s"))
|
||||
def _cli_log_handler(level, fmt):
|
||||
handler = colored_logging.StreamHandler()
|
||||
handler.setFormatter(logging.Formatter(fmt))
|
||||
handler.setLevel(level)
|
||||
return handler
|
||||
|
||||
|
|
@ -641,7 +700,7 @@ def setup_logging(config):
|
|||
level = -config.verbose_count * 10
|
||||
file_handler, log_file_path = setup_log_file_handler(
|
||||
config, logfile=logfile, fmt=file_fmt)
|
||||
cli_handler = _cli_log_handler(config, level, cli_fmt)
|
||||
cli_handler = _cli_log_handler(level, cli_fmt)
|
||||
|
||||
# TODO: use fileConfig?
|
||||
|
||||
|
|
@ -665,10 +724,8 @@ def _handle_exception(exc_type, exc_value, trace, config):
|
|||
to the user. sys.exit is always called with a nonzero status.
|
||||
|
||||
"""
|
||||
logger.debug(
|
||||
"Exiting abnormally:%s%s",
|
||||
os.linesep,
|
||||
"".join(traceback.format_exception(exc_type, exc_value, trace)))
|
||||
tb_str = "".join(traceback.format_exception(exc_type, exc_value, trace))
|
||||
logger.debug("Exiting abnormally:%s%s", os.linesep, tb_str)
|
||||
|
||||
if issubclass(exc_type, Exception) and (config is None or not config.debug):
|
||||
if config is None:
|
||||
|
|
@ -677,9 +734,11 @@ def _handle_exception(exc_type, exc_value, trace, config):
|
|||
with open(logfile, "w") as logfd:
|
||||
traceback.print_exception(
|
||||
exc_type, exc_value, trace, file=logfd)
|
||||
assert "--debug" not in sys.argv # config is None if this explodes
|
||||
except: # pylint: disable=bare-except
|
||||
sys.exit("".join(
|
||||
traceback.format_exception(exc_type, exc_value, trace)))
|
||||
sys.exit(tb_str)
|
||||
if "--debug" in sys.argv:
|
||||
sys.exit(tb_str)
|
||||
|
||||
if issubclass(exc_type, errors.Error):
|
||||
sys.exit(exc_value)
|
||||
|
|
@ -687,10 +746,7 @@ def _handle_exception(exc_type, exc_value, trace, config):
|
|||
# Here we're passing a client or ACME error out to the client at the shell
|
||||
# Tell the user a bit about what happened, without overwhelming
|
||||
# them with a full traceback
|
||||
if issubclass(exc_type, dialog.error):
|
||||
err = exc_value.complete_message()
|
||||
else:
|
||||
err = traceback.format_exception_only(exc_type, exc_value)[0]
|
||||
err = traceback.format_exception_only(exc_type, exc_value)[0]
|
||||
# Typical error from the ACME module:
|
||||
# acme.messages.Error: urn:ietf:params:acme:error:malformed :: The
|
||||
# request message was malformed :: Error creating new registration
|
||||
|
|
@ -707,8 +763,7 @@ def _handle_exception(exc_type, exc_value, trace, config):
|
|||
msg += "logfiles in {0} for more details.".format(config.logs_dir)
|
||||
sys.exit(msg)
|
||||
else:
|
||||
sys.exit("".join(
|
||||
traceback.format_exception(exc_type, exc_value, trace)))
|
||||
sys.exit(tb_str)
|
||||
|
||||
|
||||
def make_or_verify_core_dir(directory, mode, uid, strict):
|
||||
|
|
@ -727,6 +782,44 @@ def make_or_verify_core_dir(directory, mode, uid, strict):
|
|||
except OSError as error:
|
||||
raise errors.Error(_PERM_ERR_FMT.format(error))
|
||||
|
||||
def make_or_verify_needed_dirs(config):
|
||||
"""Create or verify existance of config, work, or logs directories"""
|
||||
make_or_verify_core_dir(config.config_dir, constants.CONFIG_DIRS_MODE,
|
||||
os.geteuid(), config.strict_permissions)
|
||||
make_or_verify_core_dir(config.work_dir, constants.CONFIG_DIRS_MODE,
|
||||
os.geteuid(), config.strict_permissions)
|
||||
# TODO: logs might contain sensitive data such as contents of the
|
||||
# private key! #525
|
||||
make_or_verify_core_dir(config.logs_dir, 0o700,
|
||||
os.geteuid(), config.strict_permissions)
|
||||
|
||||
|
||||
def set_displayer(config):
|
||||
"""Set the displayer"""
|
||||
if config.quiet:
|
||||
config.noninteractive_mode = True
|
||||
displayer = display_util.NoninteractiveDisplay(open(os.devnull, "w"))
|
||||
elif config.noninteractive_mode:
|
||||
displayer = display_util.NoninteractiveDisplay(sys.stdout)
|
||||
else:
|
||||
displayer = display_util.FileDisplay(sys.stdout,
|
||||
config.force_interactive)
|
||||
zope.component.provideUtility(displayer)
|
||||
|
||||
def _post_logging_setup(config, plugins, cli_args):
|
||||
"""Perform any setup or configuration tasks that require a logger."""
|
||||
|
||||
# This needs logging, but would otherwise be in HelpfulArgumentParser
|
||||
if config.validate_hooks:
|
||||
hooks.validate_hooks(config)
|
||||
|
||||
cli.possible_deprecation_warning(config)
|
||||
|
||||
logger.debug("certbot version: %s", certbot.__version__)
|
||||
# do not log `config`, as it contains sensitive data (e.g. revoke --key)!
|
||||
logger.debug("Arguments: %r", cli_args)
|
||||
logger.debug("Discovered plugins: %r", plugins)
|
||||
|
||||
|
||||
def main(cli_args=sys.argv[1:]):
|
||||
"""Command line argument parsing and main script execution."""
|
||||
|
|
@ -738,37 +831,17 @@ def main(cli_args=sys.argv[1:]):
|
|||
config = configuration.NamespaceConfig(args)
|
||||
zope.component.provideUtility(config)
|
||||
|
||||
make_or_verify_core_dir(config.config_dir, constants.CONFIG_DIRS_MODE,
|
||||
os.geteuid(), config.strict_permissions)
|
||||
make_or_verify_core_dir(config.work_dir, constants.CONFIG_DIRS_MODE,
|
||||
os.geteuid(), config.strict_permissions)
|
||||
# TODO: logs might contain sensitive data such as contents of the
|
||||
# private key! #525
|
||||
make_or_verify_core_dir(config.logs_dir, 0o700,
|
||||
os.geteuid(), config.strict_permissions)
|
||||
make_or_verify_needed_dirs(config)
|
||||
|
||||
# Setup logging ASAP, otherwise "No handlers could be found for
|
||||
# logger ..." TODO: this should be done before plugins discovery
|
||||
setup_logging(config)
|
||||
cli.possible_deprecation_warning(config)
|
||||
|
||||
logger.debug("certbot version: %s", certbot.__version__)
|
||||
# do not log `config`, as it contains sensitive data (e.g. revoke --key)!
|
||||
logger.debug("Arguments: %r", cli_args)
|
||||
logger.debug("Discovered plugins: %r", plugins)
|
||||
_post_logging_setup(config, plugins, cli_args)
|
||||
|
||||
sys.excepthook = functools.partial(_handle_exception, config=config)
|
||||
|
||||
# Displayer
|
||||
if config.quiet:
|
||||
config.noninteractive_mode = True
|
||||
displayer = display_util.NoninteractiveDisplay(open(os.devnull, "w"))
|
||||
elif config.noninteractive_mode:
|
||||
displayer = display_util.NoninteractiveDisplay(sys.stdout)
|
||||
elif config.text_mode:
|
||||
displayer = display_util.FileDisplay(sys.stdout)
|
||||
else:
|
||||
displayer = display_util.NcursesDisplay()
|
||||
zope.component.provideUtility(displayer)
|
||||
set_displayer(config)
|
||||
|
||||
# Reporter
|
||||
report = reporter.Reporter(config)
|
||||
|
|
|
|||
120
certbot/ocsp.py
Normal file
120
certbot/ocsp.py
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
"""Tools for checking certificate revocation."""
|
||||
import logging
|
||||
import re
|
||||
|
||||
from subprocess import Popen, PIPE
|
||||
|
||||
from certbot import errors
|
||||
from certbot import util
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class RevocationChecker(object):
|
||||
"This class figures out OCSP checking on this system, and performs it."
|
||||
|
||||
def __init__(self):
|
||||
self.broken = False
|
||||
|
||||
if not util.exe_exists("openssl"):
|
||||
logging.info("openssl not installed, can't check revocation")
|
||||
self.broken = True
|
||||
return
|
||||
|
||||
# New versions of openssl want -header var=val, old ones want -header var val
|
||||
test_host_format = Popen(["openssl", "ocsp", "-header", "var", "val"],
|
||||
stdout=PIPE, stderr=PIPE, universal_newlines=True)
|
||||
_out, err = test_host_format.communicate()
|
||||
if "Missing =" in err:
|
||||
self.host_args = lambda host: ["Host=" + host]
|
||||
else:
|
||||
self.host_args = lambda host: ["Host", host]
|
||||
|
||||
|
||||
def ocsp_revoked(self, cert_path, chain_path):
|
||||
"""Get revoked status for a particular cert version.
|
||||
|
||||
.. todo:: Make this a non-blocking call
|
||||
|
||||
:param str cert_path: Path to certificate
|
||||
:param str chain_path: Path to intermediate cert
|
||||
:rtype bool or None:
|
||||
:returns: True if revoked; False if valid or the check failed
|
||||
|
||||
"""
|
||||
if self.broken:
|
||||
return False
|
||||
|
||||
|
||||
url, host = self.determine_ocsp_server(cert_path)
|
||||
if not host:
|
||||
return False
|
||||
# jdkasten thanks "Bulletproof SSL and TLS - Ivan Ristic" for documenting this!
|
||||
cmd = ["openssl", "ocsp",
|
||||
"-no_nonce",
|
||||
"-issuer", chain_path,
|
||||
"-cert", cert_path,
|
||||
"-url", url,
|
||||
"-CAfile", chain_path,
|
||||
"-verify_other", chain_path,
|
||||
"-trust_other",
|
||||
"-header"] + self.host_args(host)
|
||||
logger.debug("Querying OCSP for %s", cert_path)
|
||||
logger.debug(" ".join(cmd))
|
||||
try:
|
||||
output, err = util.run_script(cmd, log=logging.debug)
|
||||
except errors.SubprocessError:
|
||||
logger.info("OCSP check failed for %s (are we offline?)", cert_path)
|
||||
return False
|
||||
|
||||
return _translate_ocsp_query(cert_path, output, err)
|
||||
|
||||
|
||||
def determine_ocsp_server(self, cert_path):
|
||||
"""Extract the OCSP server host from a certificate.
|
||||
|
||||
:param str cert_path: Path to the cert we're checking OCSP for
|
||||
:rtype tuple:
|
||||
:returns: (OCSP server URL or None, OCSP server host or None)
|
||||
|
||||
"""
|
||||
try:
|
||||
url, _err = util.run_script(
|
||||
["openssl", "x509", "-in", cert_path, "-noout", "-ocsp_uri"],
|
||||
log=logging.debug)
|
||||
except errors.SubprocessError:
|
||||
logger.info("Cannot extract OCSP URI from %s", cert_path)
|
||||
return None, None
|
||||
|
||||
url = url.rstrip()
|
||||
host = url.partition("://")[2].rstrip("/")
|
||||
if host:
|
||||
return url, host
|
||||
else:
|
||||
logger.info("Cannot process OCSP host from URL (%s) in cert at %s", url, cert_path)
|
||||
return None, None
|
||||
|
||||
def _translate_ocsp_query(cert_path, ocsp_output, ocsp_errors):
|
||||
"""Parse openssl's weird output to work out what it means."""
|
||||
|
||||
states = ("good", "revoked", "unknown")
|
||||
patterns = [r"{0}: (WARNING.*)?{1}".format(cert_path, s) for s in states]
|
||||
good, revoked, unknown = (re.search(p, ocsp_output, flags=re.DOTALL) for p in patterns)
|
||||
|
||||
warning = good.group(1) if good else None
|
||||
|
||||
if (not "Response verify OK" in ocsp_errors) or (good and warning) or unknown:
|
||||
logger.info("Revocation status for %s is unknown", cert_path)
|
||||
logger.debug("Uncertain output:\n%s\nstderr:\n%s", ocsp_output, ocsp_errors)
|
||||
return False
|
||||
elif good and not warning:
|
||||
return False
|
||||
elif revoked:
|
||||
warning = revoked.group(1)
|
||||
if warning:
|
||||
logger.info("OCSP revocation warning: %s", warning)
|
||||
return True
|
||||
else:
|
||||
logger.warn("Unable to properly parse OCSP output: %s\nstderr:%s",
|
||||
ocsp_output, ocsp_errors)
|
||||
return False
|
||||
|
||||
|
|
@ -127,17 +127,18 @@ class Addr(object):
|
|||
return "%s:%s" % self.tup
|
||||
return self.tup[0]
|
||||
|
||||
def normalized_tuple(self):
|
||||
"""Normalized representation of addr/port tuple
|
||||
"""
|
||||
if self.ipv6:
|
||||
return (self._normalize_ipv6(self.tup[0]), self.tup[1])
|
||||
return self.tup
|
||||
|
||||
def __eq__(self, other):
|
||||
if isinstance(other, self.__class__):
|
||||
if self.ipv6:
|
||||
# compare normalized to take different
|
||||
# styles of representation into account
|
||||
return (other.ipv6 and
|
||||
self._normalize_ipv6(self.tup[0]) ==
|
||||
self._normalize_ipv6(other.tup[0]) and
|
||||
self.tup[1] == other.tup[1])
|
||||
else:
|
||||
return self.tup == other.tup
|
||||
# compare normalized to take different
|
||||
# styles of representation into account
|
||||
return self.normalized_tuple() == other.normalized_tuple()
|
||||
|
||||
return False
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ from acme import jose
|
|||
from certbot import achallenges
|
||||
|
||||
from certbot.tests import acme_util
|
||||
from certbot.tests import test_util
|
||||
from certbot.tests import util as test_util
|
||||
|
||||
|
||||
class NamespaceFunctionsTest(unittest.TestCase):
|
||||
|
|
|
|||
|
|
@ -53,6 +53,14 @@ class PluginEntryPoint(object):
|
|||
"""Description with name. Handy for UI."""
|
||||
return "{0} ({1})".format(self.description, self.name)
|
||||
|
||||
@property
|
||||
def long_description(self):
|
||||
"""Long description of the plugin."""
|
||||
try:
|
||||
return self.plugin_cls.long_description
|
||||
except AttributeError:
|
||||
return self.description
|
||||
|
||||
@property
|
||||
def hidden(self):
|
||||
"""Should this plugin be hidden from UI?"""
|
||||
|
|
|
|||
|
|
@ -63,6 +63,18 @@ class PluginEntryPointTest(unittest.TestCase):
|
|||
self.assertEqual(
|
||||
"Desc (sa)", self.plugin_ep.description_with_name)
|
||||
|
||||
def test_long_description(self):
|
||||
self.plugin_ep.plugin_cls = mock.MagicMock(
|
||||
long_description="Long desc")
|
||||
self.assertEqual(
|
||||
"Long desc", self.plugin_ep.long_description)
|
||||
|
||||
def test_long_description_nonexistent(self):
|
||||
self.plugin_ep.plugin_cls = mock.MagicMock(
|
||||
description="Long desc not found", spec=["description"])
|
||||
self.assertEqual(
|
||||
"Long desc not found", self.plugin_ep.long_description)
|
||||
|
||||
def test_ifaces(self):
|
||||
self.assertTrue(self.plugin_ep.ifaces((interfaces.IAuthenticator,)))
|
||||
self.assertFalse(self.plugin_ep.ifaces((interfaces.IInstaller,)))
|
||||
|
|
|
|||
|
|
@ -1,57 +1,49 @@
|
|||
"""Manual plugin."""
|
||||
"""Manual authenticator plugin"""
|
||||
import os
|
||||
import logging
|
||||
import pipes
|
||||
import shutil
|
||||
import signal
|
||||
import socket
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
|
||||
import six
|
||||
import zope.component
|
||||
import zope.interface
|
||||
|
||||
from acme import challenges
|
||||
from acme import errors as acme_errors
|
||||
|
||||
from certbot import errors
|
||||
from certbot import interfaces
|
||||
from certbot import errors
|
||||
from certbot import hooks
|
||||
from certbot.plugins import common
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@zope.interface.implementer(interfaces.IAuthenticator)
|
||||
@zope.interface.provider(interfaces.IPluginFactory)
|
||||
class Authenticator(common.Plugin):
|
||||
"""Manual Authenticator.
|
||||
"""Manual authenticator
|
||||
|
||||
This plugin requires user's manual intervention in setting up a HTTP
|
||||
server for solving http-01 challenges and thus does not need to be
|
||||
run as a privileged process. Alternatively shows instructions on how
|
||||
to use Python's built-in HTTP server.
|
||||
|
||||
.. todo:: Support for `~.challenges.TLSSNI01`.
|
||||
This plugin allows the user to perform the domain validation
|
||||
challenge(s) themselves. This either be done manually by the user or
|
||||
through shell scripts provided to Certbot.
|
||||
|
||||
"""
|
||||
|
||||
description = 'Manual configuration or run your own shell scripts'
|
||||
hidden = True
|
||||
|
||||
description = "Manually configure an HTTP server"
|
||||
|
||||
MESSAGE_TEMPLATE = {
|
||||
"dns-01": """\
|
||||
long_description = (
|
||||
'Authenticate through manual configuration or custom shell scripts. '
|
||||
'When using shell scripts, an authenticator script must be provided. '
|
||||
'The environment variables available to this script are '
|
||||
'$CERTBOT_DOMAIN which contains the domain being authenticated, '
|
||||
'$CERTBOT_VALIDATION which is the validation string, and '
|
||||
'$CERTBOT_TOKEN which is the filename of the resource requested when '
|
||||
'performing an HTTP-01 challenge. An additional cleanup script can '
|
||||
'also be provided and can use the additional variable '
|
||||
'$CERTBOT_AUTH_OUTPUT which contains the stdout output from the auth '
|
||||
'script.')
|
||||
_DNS_INSTRUCTIONS = """\
|
||||
Please deploy a DNS TXT record under the name
|
||||
{domain} with the following value:
|
||||
|
||||
{validation}
|
||||
|
||||
Once this is deployed,
|
||||
""",
|
||||
"http-01": """\
|
||||
Once this is deployed,"""
|
||||
_HTTP_INSTRUCTIONS = """\
|
||||
Make sure your web server displays the following content at
|
||||
{uri} before continuing:
|
||||
|
||||
|
|
@ -60,203 +52,114 @@ Make sure your web server displays the following content at
|
|||
If you don't have HTTP server configured, you can run the following
|
||||
command on the target server (as root):
|
||||
|
||||
{command}
|
||||
"""}
|
||||
|
||||
# a disclaimer about your current IP being transmitted to Let's Encrypt's servers.
|
||||
IP_DISCLAIMER = """\
|
||||
NOTE: The IP of this machine will be publicly logged as having requested this certificate. \
|
||||
If you're running certbot in manual mode on a machine that is not your server, \
|
||||
please ensure you're okay with that.
|
||||
|
||||
Are you OK with your IP being logged?
|
||||
"""
|
||||
|
||||
# "cd /tmp/certbot" makes sure user doesn't serve /root,
|
||||
# separate "public_html" ensures that cert.pem/key.pem are not
|
||||
# served and makes it more obvious that Python command will serve
|
||||
# anything recursively under the cwd
|
||||
|
||||
CMD_TEMPLATE = """\
|
||||
mkdir -p {root}/public_html/{achall.URI_ROOT_PATH}
|
||||
cd {root}/public_html
|
||||
mkdir -p /tmp/certbot/public_html/{achall.URI_ROOT_PATH}
|
||||
cd /tmp/certbot/public_html
|
||||
printf "%s" {validation} > {achall.URI_ROOT_PATH}/{encoded_token}
|
||||
# run only once per server:
|
||||
$(command -v python2 || command -v python2.7 || command -v python2.6) -c \\
|
||||
"import BaseHTTPServer, SimpleHTTPServer; \\
|
||||
s = BaseHTTPServer.HTTPServer(('', {port}), SimpleHTTPServer.SimpleHTTPRequestHandler); \\
|
||||
s.serve_forever()" """
|
||||
"""Command template."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(Authenticator, self).__init__(*args, **kwargs)
|
||||
self._root = (tempfile.mkdtemp() if self.conf("test-mode")
|
||||
else "/tmp/certbot")
|
||||
self._httpd = None
|
||||
self.env = dict()
|
||||
|
||||
@classmethod
|
||||
def add_parser_arguments(cls, add):
|
||||
add("test-mode", action="store_true",
|
||||
help="Test mode. Executes the manual command in subprocess.")
|
||||
add("public-ip-logging-ok", action="store_true",
|
||||
help="Automatically allows public IP logging.")
|
||||
add('auth-hook',
|
||||
help='Path or command to execute for the authentication script')
|
||||
add('cleanup-hook',
|
||||
help='Path or command to execute for the cleanup script')
|
||||
add('public-ip-logging-ok', action='store_true',
|
||||
help='Automatically allows public IP logging (default: Ask)')
|
||||
|
||||
def prepare(self): # pylint: disable=missing-docstring,no-self-use
|
||||
if self.config.noninteractive_mode and not self.conf("test-mode"):
|
||||
raise errors.PluginError("Running manual mode non-interactively is not supported")
|
||||
def prepare(self): # pylint: disable=missing-docstring
|
||||
if self.config.noninteractive_mode and not self.conf('auth-hook'):
|
||||
raise errors.PluginError(
|
||||
'An authentication script must be provided with --{0} when '
|
||||
'using the manual plugin non-interactively.'.format(
|
||||
self.option_name('auth-hook')))
|
||||
self._validate_hooks()
|
||||
|
||||
def _validate_hooks(self):
|
||||
if self.config.validate_hooks:
|
||||
for name in ('auth-hook', 'cleanup-hook'):
|
||||
hook = self.conf(name)
|
||||
if hook is not None:
|
||||
hook_prefix = self.option_name(name)[:-len('-hook')]
|
||||
hooks.validate_hook(hook, hook_prefix)
|
||||
|
||||
def more_info(self): # pylint: disable=missing-docstring,no-self-use
|
||||
return ("This plugin requires user's manual intervention in setting "
|
||||
"up challenges to prove control of a domain and does not need "
|
||||
"to be run as a privileged process. When solving "
|
||||
"http-01 challenges, the user is responsible for setting up "
|
||||
"an HTTP server. Alternatively, instructions are shown on how "
|
||||
"to use Python's built-in HTTP server. The user is "
|
||||
"responsible for configuration of a domain's DNS when solving "
|
||||
"dns-01 challenges. The type of challenges used can be "
|
||||
"controlled through the --preferred-challenges flag.")
|
||||
return (
|
||||
'This plugin allows the user to customize setup for domain '
|
||||
'validation challenges either through shell scripts provided by '
|
||||
'the user or by performing the setup manually.')
|
||||
|
||||
def get_chall_pref(self, domain):
|
||||
# pylint: disable=missing-docstring,no-self-use,unused-argument
|
||||
return [challenges.HTTP01, challenges.DNS01]
|
||||
|
||||
def perform(self, achalls):
|
||||
# pylint: disable=missing-docstring
|
||||
self._get_ip_logging_permission()
|
||||
mapping = {"http-01": self._perform_http01_challenge,
|
||||
"dns-01": self._perform_dns01_challenge}
|
||||
def perform(self, achalls): # pylint: disable=missing-docstring
|
||||
self._verify_ip_logging_ok()
|
||||
|
||||
if self.conf('auth-hook'):
|
||||
perform_achall = self._perform_achall_with_script
|
||||
else:
|
||||
perform_achall = self._perform_achall_manually
|
||||
|
||||
responses = []
|
||||
# TODO: group achalls by the same socket.gethostbyname(_ex)
|
||||
# and prompt only once per server (one "echo -n" per domain)
|
||||
for achall in achalls:
|
||||
responses.append(mapping[achall.typ](achall))
|
||||
perform_achall(achall)
|
||||
responses.append(achall.response(achall.account_key))
|
||||
return responses
|
||||
|
||||
@classmethod
|
||||
def _test_mode_busy_wait(cls, port):
|
||||
while True:
|
||||
time.sleep(1)
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
try:
|
||||
sock.connect(("localhost", port))
|
||||
except socket.error: # pragma: no cover
|
||||
pass
|
||||
def _verify_ip_logging_ok(self):
|
||||
if not self.conf('public-ip-logging-ok'):
|
||||
cli_flag = '--{0}'.format(self.option_name('public-ip-logging-ok'))
|
||||
msg = ('NOTE: The IP of this machine will be publicly logged as '
|
||||
"having requested this certificate. If you're running "
|
||||
'certbot in manual mode on a machine that is not your '
|
||||
"server, please ensure you're okay with that.\n\n"
|
||||
'Are you OK with your IP being logged?')
|
||||
display = zope.component.getUtility(interfaces.IDisplay)
|
||||
if display.yesno(msg, cli_flag=cli_flag, force_interactive=True):
|
||||
setattr(self.config, self.dest('public-ip-logging-ok'), True)
|
||||
else:
|
||||
break
|
||||
finally:
|
||||
sock.close()
|
||||
raise errors.PluginError('Must agree to IP logging to proceed')
|
||||
|
||||
def cleanup(self, achalls):
|
||||
# pylint: disable=missing-docstring
|
||||
for achall in achalls:
|
||||
if isinstance(achall.chall, challenges.HTTP01):
|
||||
self._cleanup_http01_challenge(achall)
|
||||
|
||||
def _perform_http01_challenge(self, achall):
|
||||
# same path for each challenge response would be easier for
|
||||
# users, but will not work if multiple domains point at the
|
||||
# same server: default command doesn't support virtual hosts
|
||||
response, validation = achall.response_and_validation()
|
||||
|
||||
port = (response.port if self.config.http01_port is None
|
||||
else int(self.config.http01_port))
|
||||
command = self.CMD_TEMPLATE.format(
|
||||
root=self._root, achall=achall, response=response,
|
||||
# TODO(kuba): pipes still necessary?
|
||||
validation=pipes.quote(validation),
|
||||
encoded_token=achall.chall.encode("token"),
|
||||
port=port)
|
||||
if self.conf("test-mode"):
|
||||
logger.debug("Test mode. Executing the manual command: %s", command)
|
||||
# sh shipped with OS X does't support echo -n, but supports printf
|
||||
try:
|
||||
self._httpd = subprocess.Popen(
|
||||
command,
|
||||
# don't care about setting stdout and stderr,
|
||||
# we're in test mode anyway
|
||||
shell=True,
|
||||
executable=None,
|
||||
# "preexec_fn" is UNIX specific, but so is "command"
|
||||
preexec_fn=os.setsid)
|
||||
except OSError as error: # ValueError should not happen!
|
||||
logger.debug(
|
||||
"Couldn't execute manual command: %s", error, exc_info=True)
|
||||
return False
|
||||
logger.debug("Manual command running as PID %s.", self._httpd.pid)
|
||||
# give it some time to bootstrap, before we try to verify
|
||||
# (cert generation in case of simpleHttpS might take time)
|
||||
self._test_mode_busy_wait(port)
|
||||
|
||||
if self._httpd.poll() is not None:
|
||||
raise errors.Error("Couldn't execute manual command")
|
||||
def _perform_achall_with_script(self, achall):
|
||||
env = dict(CERTBOT_DOMAIN=achall.domain,
|
||||
CERTBOT_VALIDATION=achall.validation(achall.account_key))
|
||||
if isinstance(achall.chall, challenges.HTTP01):
|
||||
env['CERTBOT_TOKEN'] = achall.chall.encode('token')
|
||||
else:
|
||||
self._notify_and_wait(
|
||||
self._get_message(achall).format(
|
||||
validation=validation,
|
||||
response=response,
|
||||
uri=achall.chall.uri(achall.domain),
|
||||
command=command))
|
||||
os.environ.pop('CERTBOT_TOKEN', None)
|
||||
os.environ.update(env)
|
||||
_, out = hooks.execute(self.conf('auth-hook'))
|
||||
env['CERTBOT_AUTH_OUTPUT'] = out.strip()
|
||||
self.env[achall.domain] = env
|
||||
|
||||
if not response.simple_verify(
|
||||
achall.chall, achall.domain,
|
||||
achall.account_key.public_key(), self.config.http01_port):
|
||||
logger.warning("Self-verify of challenge failed.")
|
||||
|
||||
return response
|
||||
|
||||
def _perform_dns01_challenge(self, achall):
|
||||
response, validation = achall.response_and_validation()
|
||||
if not self.conf("test-mode"):
|
||||
self._notify_and_wait(
|
||||
self._get_message(achall).format(
|
||||
validation=validation,
|
||||
domain=achall.validation_domain_name(achall.domain),
|
||||
response=response))
|
||||
|
||||
try:
|
||||
verification_status = response.simple_verify(
|
||||
achall.chall, achall.domain,
|
||||
achall.account_key.public_key())
|
||||
except acme_errors.DependencyError:
|
||||
logger.warning("Self verification requires optional "
|
||||
"dependency `dnspython` to be installed.")
|
||||
def _perform_achall_manually(self, achall):
|
||||
validation = achall.validation(achall.account_key)
|
||||
if isinstance(achall.chall, challenges.HTTP01):
|
||||
msg = self._HTTP_INSTRUCTIONS.format(
|
||||
achall=achall, encoded_token=achall.chall.encode('token'),
|
||||
port=self.config.http01_port,
|
||||
uri=achall.chall.uri(achall.domain), validation=validation)
|
||||
else:
|
||||
if not verification_status:
|
||||
logger.warning("Self-verify of challenge failed.")
|
||||
assert isinstance(achall.chall, challenges.DNS01)
|
||||
msg = self._DNS_INSTRUCTIONS.format(
|
||||
domain=achall.validation_domain_name(achall.domain),
|
||||
validation=validation)
|
||||
display = zope.component.getUtility(interfaces.IDisplay)
|
||||
display.notification(msg, wrap=False, force_interactive=True)
|
||||
|
||||
return response
|
||||
|
||||
def _cleanup_http01_challenge(self, achall):
|
||||
# pylint: disable=missing-docstring,unused-argument
|
||||
if self.conf("test-mode"):
|
||||
assert self._httpd is not None, (
|
||||
"cleanup() must be called after perform()")
|
||||
if self._httpd.poll() is None:
|
||||
logger.debug("Terminating manual command process")
|
||||
os.killpg(self._httpd.pid, signal.SIGTERM)
|
||||
else:
|
||||
logger.debug("Manual command process already terminated "
|
||||
"with %s code", self._httpd.returncode)
|
||||
shutil.rmtree(self._root)
|
||||
|
||||
def _notify_and_wait(self, message):
|
||||
# pylint: disable=no-self-use
|
||||
# TODO: IDisplay wraps messages, breaking the command
|
||||
#answer = zope.component.getUtility(interfaces.IDisplay).notification(
|
||||
# message=message, height=25, pause=True)
|
||||
sys.stdout.write(message)
|
||||
six.moves.input("Press ENTER to continue")
|
||||
|
||||
def _get_ip_logging_permission(self):
|
||||
# pylint: disable=missing-docstring
|
||||
if not (self.conf("test-mode") or self.conf("public-ip-logging-ok")):
|
||||
if not zope.component.getUtility(interfaces.IDisplay).yesno(
|
||||
self.IP_DISCLAIMER, "Yes", "No",
|
||||
cli_flag="--manual-public-ip-logging-ok"):
|
||||
raise errors.PluginError("Must agree to IP logging to proceed")
|
||||
else:
|
||||
self.config.namespace.manual_public_ip_logging_ok = True
|
||||
|
||||
def _get_message(self, achall):
|
||||
# pylint: disable=missing-docstring,no-self-use,unused-argument
|
||||
return self.MESSAGE_TEMPLATE.get(achall.chall.typ, "")
|
||||
def cleanup(self, achalls): # pylint: disable=missing-docstring
|
||||
if self.conf('cleanup-hook'):
|
||||
for achall in achalls:
|
||||
env = self.env.pop(achall.domain)
|
||||
if 'CERTBOT_TOKEN' not in env:
|
||||
os.environ.pop('CERTBOT_TOKEN', None)
|
||||
os.environ.update(env)
|
||||
hooks.execute(self.conf('cleanup-hook'))
|
||||
|
|
|
|||
|
|
@ -1,136 +1,114 @@
|
|||
"""Tests for certbot.plugins.manual."""
|
||||
import signal
|
||||
"""Tests for certbot.plugins.manual"""
|
||||
import os
|
||||
import unittest
|
||||
|
||||
import six
|
||||
import mock
|
||||
|
||||
from acme import challenges
|
||||
from acme import errors as acme_errors
|
||||
from acme import jose
|
||||
|
||||
from certbot import achallenges
|
||||
from certbot import errors
|
||||
|
||||
from certbot.tests import acme_util
|
||||
from certbot.tests import test_util
|
||||
|
||||
|
||||
KEY = jose.JWKRSA.load(test_util.load_vector("rsa512_key.pem"))
|
||||
from certbot.tests import util as test_util
|
||||
|
||||
|
||||
class AuthenticatorTest(unittest.TestCase):
|
||||
"""Tests for certbot.plugins.manual.Authenticator."""
|
||||
|
||||
def setUp(self):
|
||||
from certbot.plugins.manual import Authenticator
|
||||
self.http_achall = acme_util.HTTP01_A
|
||||
self.dns_achall = acme_util.DNS01_A
|
||||
self.achalls = [self.http_achall, self.dns_achall]
|
||||
self.config = mock.MagicMock(
|
||||
http01_port=8080, manual_test_mode=False,
|
||||
manual_public_ip_logging_ok=False, noninteractive_mode=True)
|
||||
self.auth = Authenticator(config=self.config, name="manual")
|
||||
http01_port=0, manual_auth_hook=None, manual_cleanup_hook=None,
|
||||
manual_public_ip_logging_ok=False, noninteractive_mode=False,
|
||||
validate_hooks=False)
|
||||
|
||||
self.http01 = achallenges.KeyAuthorizationAnnotatedChallenge(
|
||||
challb=acme_util.HTTP01_P, domain="foo.com", account_key=KEY)
|
||||
self.dns01 = achallenges.KeyAuthorizationAnnotatedChallenge(
|
||||
challb=acme_util.DNS01_P, domain="foo.com", account_key=KEY)
|
||||
from certbot.plugins.manual import Authenticator
|
||||
self.auth = Authenticator(self.config, name='manual')
|
||||
|
||||
self.achalls = [self.http01, self.dns01]
|
||||
|
||||
config_test_mode = mock.MagicMock(
|
||||
http01_port=8080, manual_test_mode=True, noninteractive_mode=True)
|
||||
self.auth_test_mode = Authenticator(
|
||||
config=config_test_mode, name="manual")
|
||||
|
||||
def test_prepare(self):
|
||||
def test_prepare_no_hook_noninteractive(self):
|
||||
self.config.noninteractive_mode = True
|
||||
self.assertRaises(errors.PluginError, self.auth.prepare)
|
||||
self.auth_test_mode.prepare() # error not raised
|
||||
|
||||
def test_prepare_bad_hook(self):
|
||||
self.config.manual_auth_hook = os.path.abspath(os.sep) # is / on UNIX
|
||||
self.config.validate_hooks = True
|
||||
self.assertRaises(errors.HookCommandNotFound, self.auth.prepare)
|
||||
|
||||
def test_more_info(self):
|
||||
self.assertTrue(isinstance(self.auth.more_info(), str))
|
||||
self.assertTrue(isinstance(self.auth.more_info(), six.string_types))
|
||||
|
||||
def test_get_chall_pref(self):
|
||||
self.assertTrue(all(issubclass(pref, challenges.Challenge)
|
||||
for pref in self.auth.get_chall_pref("foo.com")))
|
||||
self.assertEqual(self.auth.get_chall_pref('example.org'),
|
||||
[challenges.HTTP01, challenges.DNS01])
|
||||
|
||||
@mock.patch("certbot.plugins.manual.zope.component.getUtility")
|
||||
def test_perform_empty(self, mock_interaction):
|
||||
mock_interaction().yesno.return_value = True
|
||||
self.assertEqual([], self.auth.perform([]))
|
||||
@test_util.patch_get_utility()
|
||||
def test_ip_logging_not_ok(self, mock_get_utility):
|
||||
mock_get_utility().yesno.return_value = False
|
||||
self.assertRaises(errors.PluginError, self.auth.perform, [])
|
||||
|
||||
@mock.patch("certbot.plugins.manual.zope.component.getUtility")
|
||||
@mock.patch("certbot.plugins.manual.sys.stdout")
|
||||
@mock.patch("acme.challenges.HTTP01Response.simple_verify")
|
||||
@mock.patch("six.moves.input")
|
||||
def test_perform(self, mock_raw_input, mock_verify, mock_stdout, mock_interaction):
|
||||
mock_verify.return_value = True
|
||||
mock_interaction().yesno.return_value = True
|
||||
@test_util.patch_get_utility()
|
||||
def test_ip_logging_ok(self, mock_get_utility):
|
||||
mock_get_utility().yesno.return_value = True
|
||||
self.auth.perform([])
|
||||
self.assertTrue(self.config.manual_public_ip_logging_ok)
|
||||
|
||||
resp_http = self.http01.response(KEY)
|
||||
resp_dns = self.dns01.response(KEY)
|
||||
def test_script_perform(self):
|
||||
self.config.manual_public_ip_logging_ok = True
|
||||
self.config.manual_auth_hook = (
|
||||
'echo $CERTBOT_DOMAIN; echo ${CERTBOT_TOKEN:-notoken}; '
|
||||
'echo $CERTBOT_VALIDATION;')
|
||||
dns_expected = '{0}\n{1}\n{2}'.format(
|
||||
self.dns_achall.domain, 'notoken',
|
||||
self.dns_achall.validation(self.dns_achall.account_key))
|
||||
http_expected = '{0}\n{1}\n{2}'.format(
|
||||
self.http_achall.domain, self.http_achall.chall.encode('token'),
|
||||
self.http_achall.validation(self.http_achall.account_key))
|
||||
|
||||
self.assertEqual([resp_http, resp_dns], self.auth.perform(self.achalls))
|
||||
self.assertEqual(2, mock_raw_input.call_count)
|
||||
mock_verify.assert_called_with(
|
||||
self.http01.challb.chall, "foo.com", KEY.public_key(), 8080)
|
||||
self.assertEqual(
|
||||
self.auth.perform(self.achalls),
|
||||
[achall.response(achall.account_key) for achall in self.achalls])
|
||||
self.assertEqual(
|
||||
self.auth.env[self.dns_achall.domain]['CERTBOT_AUTH_OUTPUT'],
|
||||
dns_expected)
|
||||
self.assertEqual(
|
||||
self.auth.env[self.http_achall.domain]['CERTBOT_AUTH_OUTPUT'],
|
||||
http_expected)
|
||||
|
||||
message = mock_stdout.write.mock_calls[0][1][0]
|
||||
self.assertTrue(self.http01.chall.encode("token") in message)
|
||||
@test_util.patch_get_utility()
|
||||
def test_manual_perform(self, mock_get_utility):
|
||||
self.config.manual_public_ip_logging_ok = True
|
||||
self.assertEqual(
|
||||
self.auth.perform(self.achalls),
|
||||
[achall.response(achall.account_key) for achall in self.achalls])
|
||||
for i, (args, kwargs) in enumerate(mock_get_utility().notification.call_args_list):
|
||||
achall = self.achalls[i]
|
||||
self.assertTrue(achall.validation(achall.account_key) in args[0])
|
||||
self.assertFalse(kwargs['wrap'])
|
||||
|
||||
mock_verify.return_value = False
|
||||
with mock.patch("certbot.plugins.manual.logger") as mock_logger:
|
||||
self.auth.perform(self.achalls)
|
||||
self.assertEqual(2, mock_logger.warning.call_count)
|
||||
def test_cleanup(self):
|
||||
self.config.manual_public_ip_logging_ok = True
|
||||
self.config.manual_auth_hook = 'echo foo;'
|
||||
self.config.manual_cleanup_hook = '# cleanup'
|
||||
self.auth.perform(self.achalls)
|
||||
|
||||
@mock.patch("certbot.plugins.manual.zope.component.getUtility")
|
||||
@mock.patch("acme.challenges.DNS01Response.simple_verify")
|
||||
@mock.patch("six.moves.input")
|
||||
def test_perform_missing_dependency(self, mock_raw_input, mock_verify, mock_interaction):
|
||||
mock_interaction().yesno.return_value = True
|
||||
mock_verify.side_effect = acme_errors.DependencyError()
|
||||
for achall in self.achalls:
|
||||
self.auth.cleanup([achall])
|
||||
self.assertEqual(os.environ['CERTBOT_AUTH_OUTPUT'], 'foo')
|
||||
self.assertEqual(os.environ['CERTBOT_DOMAIN'], achall.domain)
|
||||
self.assertEqual(
|
||||
os.environ['CERTBOT_VALIDATION'],
|
||||
achall.validation(achall.account_key))
|
||||
|
||||
with mock.patch("certbot.plugins.manual.logger") as mock_logger:
|
||||
self.auth.perform([self.dns01])
|
||||
self.assertEqual(1, mock_logger.warning.call_count)
|
||||
|
||||
mock_raw_input.assert_called_once_with("Press ENTER to continue")
|
||||
|
||||
@mock.patch("certbot.plugins.manual.zope.component.getUtility")
|
||||
@mock.patch("certbot.plugins.manual.Authenticator._notify_and_wait")
|
||||
def test_disagree_with_ip_logging(self, mock_notify, mock_interaction):
|
||||
mock_interaction().yesno.return_value = False
|
||||
mock_notify.side_effect = errors.Error("Exception not raised, \
|
||||
continued execution even after disagreeing with IP logging")
|
||||
|
||||
self.assertRaises(errors.PluginError, self.auth.perform, self.achalls)
|
||||
|
||||
@mock.patch("certbot.plugins.manual.subprocess.Popen", autospec=True)
|
||||
def test_perform_test_command_oserror(self, mock_popen):
|
||||
mock_popen.side_effect = OSError
|
||||
self.assertEqual([False], self.auth_test_mode.perform([self.http01]))
|
||||
|
||||
@mock.patch("certbot.plugins.manual.socket.socket")
|
||||
@mock.patch("certbot.plugins.manual.time.sleep", autospec=True)
|
||||
@mock.patch("certbot.plugins.manual.subprocess.Popen", autospec=True)
|
||||
def test_perform_test_command_run_failure(
|
||||
self, mock_popen, unused_mock_sleep, unused_mock_socket):
|
||||
mock_popen.poll.return_value = 10
|
||||
mock_popen.return_value.pid = 1234
|
||||
self.assertRaises(
|
||||
errors.Error, self.auth_test_mode.perform, self.achalls)
|
||||
|
||||
def test_cleanup_test_mode_already_terminated(self):
|
||||
# pylint: disable=protected-access
|
||||
self.auth_test_mode._httpd = httpd = mock.Mock()
|
||||
httpd.poll.return_value = 0
|
||||
self.auth_test_mode.cleanup(self.achalls)
|
||||
|
||||
@mock.patch("certbot.plugins.manual.os.killpg", autospec=True)
|
||||
def test_cleanup_test_mode_kills_still_running(self, mock_killpg):
|
||||
# pylint: disable=protected-access
|
||||
self.auth_test_mode._httpd = httpd = mock.Mock(pid=1234)
|
||||
httpd.poll.return_value = None
|
||||
self.auth_test_mode.cleanup(self.achalls)
|
||||
mock_killpg.assert_called_once_with(1234, signal.SIGTERM)
|
||||
if isinstance(achall.chall, challenges.HTTP01):
|
||||
self.assertEqual(
|
||||
os.environ['CERTBOT_TOKEN'],
|
||||
achall.chall.encode('token'))
|
||||
else:
|
||||
self.assertFalse('CERTBOT_TOKEN' in os.environ)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if __name__ == '__main__':
|
||||
unittest.main() # pragma: no cover
|
||||
|
|
|
|||
|
|
@ -40,9 +40,6 @@ class Installer(common.Plugin):
|
|||
def supported_enhancements(self):
|
||||
return []
|
||||
|
||||
def get_all_certs_keys(self):
|
||||
return []
|
||||
|
||||
def save(self, title=None, temporary=False):
|
||||
pass # pragma: no cover
|
||||
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@ class InstallerTest(unittest.TestCase):
|
|||
self.assertTrue(isinstance(self.installer.more_info(), str))
|
||||
self.assertEqual([], self.installer.get_all_names())
|
||||
self.assertEqual([], self.installer.supported_enhancements())
|
||||
self.assertEqual([], self.installer.get_all_certs_keys())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
|||
|
|
@ -111,7 +111,8 @@ def choose_plugin(prepared, question):
|
|||
|
||||
while True:
|
||||
disp = z_util(interfaces.IDisplay)
|
||||
code, index = disp.menu(question, opts, help_label="More Info")
|
||||
code, index = disp.menu(
|
||||
question, opts, help_label="More Info", force_interactive=True)
|
||||
|
||||
if code == display_util.OK:
|
||||
plugin_ep = prepared[index]
|
||||
|
|
@ -119,8 +120,7 @@ def choose_plugin(prepared, question):
|
|||
z_util(interfaces.IDisplay).notification(
|
||||
"The selected plugin encountered an error while parsing "
|
||||
"your server configuration and cannot be used. The error "
|
||||
"was:\n\n{0}".format(plugin_ep.prepare()),
|
||||
height=display_util.HEIGHT, pause=False)
|
||||
"was:\n\n{0}".format(plugin_ep.prepare()), pause=False)
|
||||
else:
|
||||
return plugin_ep
|
||||
elif code == display_util.HELP:
|
||||
|
|
@ -128,8 +128,8 @@ def choose_plugin(prepared, question):
|
|||
msg = "Reported Error: %s" % prepared[index].prepare()
|
||||
else:
|
||||
msg = prepared[index].init().more_info()
|
||||
z_util(interfaces.IDisplay).notification(
|
||||
msg, height=display_util.HEIGHT)
|
||||
z_util(interfaces.IDisplay).notification(msg,
|
||||
force_interactive=True)
|
||||
else:
|
||||
return None
|
||||
|
||||
|
|
|
|||
|
|
@ -110,7 +110,8 @@ class ChoosePluginTest(unittest.TestCase):
|
|||
"""Tests for certbot.plugins.selection.choose_plugin."""
|
||||
|
||||
def setUp(self):
|
||||
zope.component.provideUtility(display_util.FileDisplay(sys.stdout))
|
||||
zope.component.provideUtility(display_util.FileDisplay(sys.stdout,
|
||||
False))
|
||||
self.mock_apache = mock.Mock(
|
||||
description_with_name="a", misconfigured=True)
|
||||
self.mock_stand = mock.Mock(
|
||||
|
|
|
|||
|
|
@ -243,13 +243,13 @@ class Authenticator(common.Plugin):
|
|||
"Could not bind TCP port {0} because you don't have "
|
||||
"the appropriate permissions (for example, you "
|
||||
"aren't running this program as "
|
||||
"root).".format(error.port))
|
||||
"root).".format(error.port), force_interactive=True)
|
||||
elif error.socket_error.errno == socket.errno.EADDRINUSE:
|
||||
display.notification(
|
||||
"Could not bind TCP port {0} because it is already in "
|
||||
"use by another process on this system (such as a web "
|
||||
"server). Please stop the program in question and then "
|
||||
"try again.".format(error.port))
|
||||
"try again.".format(error.port), force_interactive=True)
|
||||
else:
|
||||
raise # XXX: How to handle unknown errors in binding?
|
||||
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ from certbot import errors
|
|||
from certbot import interfaces
|
||||
|
||||
from certbot.tests import acme_util
|
||||
from certbot.tests import test_util
|
||||
from certbot.tests import util as test_util
|
||||
|
||||
|
||||
class ServerManagerTest(unittest.TestCase):
|
||||
|
|
@ -169,7 +169,7 @@ class AuthenticatorTest(unittest.TestCase):
|
|||
mock_util.already_listening.assert_called_once_with(port, False)
|
||||
mock_util.already_listening.reset_mock()
|
||||
|
||||
@mock.patch("certbot.plugins.standalone.zope.component.getUtility")
|
||||
@test_util.patch_get_utility()
|
||||
def test_perform(self, unused_mock_get_utility):
|
||||
achalls = self._get_achalls()
|
||||
|
||||
|
|
@ -177,7 +177,7 @@ class AuthenticatorTest(unittest.TestCase):
|
|||
self.assertEqual(mock.sentinel.responses, self.auth.perform(achalls))
|
||||
self.auth.perform2.assert_called_once_with(achalls)
|
||||
|
||||
@mock.patch("certbot.plugins.standalone.zope.component.getUtility")
|
||||
@test_util.patch_get_utility()
|
||||
def _test_perform_bind_errors(self, errno, achalls, mock_get_utility):
|
||||
port = get_open_port()
|
||||
def _perform2(unused_achalls):
|
||||
|
|
|
|||
|
|
@ -30,12 +30,12 @@ RENEWER_EXTRA_MSG = (
|
|||
" needing to stop and start your webserver.")
|
||||
|
||||
|
||||
def path_surgery(restart_cmd):
|
||||
"""Attempt to perform PATH surgery to find restart_cmd
|
||||
def path_surgery(cmd):
|
||||
"""Attempt to perform PATH surgery to find cmd
|
||||
|
||||
Mitigates https://github.com/certbot/certbot/issues/1833
|
||||
|
||||
:param str restart_cmd: the command that is being searched for in the PATH
|
||||
:param str cmd: the command that is being searched for in the PATH
|
||||
|
||||
:returns: True if the operation succeeded, False otherwise
|
||||
"""
|
||||
|
|
@ -49,14 +49,14 @@ def path_surgery(restart_cmd):
|
|||
|
||||
if any(added):
|
||||
logger.debug("Can't find %s, attempting PATH mitigation by adding %s",
|
||||
restart_cmd, os.pathsep.join(added))
|
||||
cmd, os.pathsep.join(added))
|
||||
os.environ["PATH"] = path
|
||||
|
||||
if util.exe_exists(restart_cmd):
|
||||
if util.exe_exists(cmd):
|
||||
return True
|
||||
else:
|
||||
expanded = " expanded" if any(added) else ""
|
||||
logger.warning("Failed to find %s in%s PATH: %s", restart_cmd,
|
||||
logger.warning("Failed to find %s in%s PATH: %s", cmd,
|
||||
expanded, path)
|
||||
return False
|
||||
|
||||
|
|
@ -103,7 +103,7 @@ def already_listening_socket(port, renewer=False):
|
|||
"Port {0} is already in use by another process. This will "
|
||||
"prevent us from binding to that port. Please stop the "
|
||||
"process that is populating the port in question and try "
|
||||
"again. {1}".format(port, extra), height=13)
|
||||
"again. {1}".format(port, extra), force_interactive=True)
|
||||
return True
|
||||
finally:
|
||||
testsocket.close()
|
||||
|
|
@ -152,7 +152,7 @@ def already_listening_psutil(port, renewer=False):
|
|||
"on TCP port {2}. This will prevent us from binding to "
|
||||
"that port. Please stop the {0} program temporarily "
|
||||
"and then try again.{3}".format(name, pid, port, extra),
|
||||
height=13)
|
||||
force_interactive=True)
|
||||
return True
|
||||
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
||||
# Perhaps the result of a race where the process could have
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import unittest
|
|||
import mock
|
||||
|
||||
from certbot.plugins.util import PSUTIL_REQUIREMENT
|
||||
from certbot.tests import test_util
|
||||
from certbot.tests import util as test_util
|
||||
|
||||
|
||||
class PathSurgeryTest(unittest.TestCase):
|
||||
|
|
@ -51,7 +51,7 @@ class AlreadyListeningTestNoPsutil(AlreadyListeningTest):
|
|||
return super(
|
||||
AlreadyListeningTestNoPsutil, cls)._call(*args, **kwargs)
|
||||
|
||||
@mock.patch("certbot.plugins.util.zope.component.getUtility")
|
||||
@test_util.patch_get_utility()
|
||||
def test_ports_available(self, mock_getutil):
|
||||
# Ensure we don't get error
|
||||
with mock.patch("socket.socket.bind"):
|
||||
|
|
@ -59,7 +59,7 @@ class AlreadyListeningTestNoPsutil(AlreadyListeningTest):
|
|||
self.assertFalse(self._call(80, True))
|
||||
self.assertEqual(mock_getutil.call_count, 0)
|
||||
|
||||
@mock.patch("certbot.plugins.util.zope.component.getUtility")
|
||||
@test_util.patch_get_utility()
|
||||
def test_ports_blocked(self, mock_getutil):
|
||||
with mock.patch("certbot.plugins.util.socket.socket.bind") as mock_bind:
|
||||
mock_bind.side_effect = socket.error
|
||||
|
|
@ -77,7 +77,7 @@ class AlreadyListeningTestPsutil(AlreadyListeningTest):
|
|||
"""Tests for certbot.plugins.already_listening."""
|
||||
@mock.patch("certbot.plugins.util.psutil.net_connections")
|
||||
@mock.patch("certbot.plugins.util.psutil.Process")
|
||||
@mock.patch("certbot.plugins.util.zope.component.getUtility")
|
||||
@test_util.patch_get_utility()
|
||||
def test_race_condition(self, mock_get_utility, mock_process, mock_net):
|
||||
# This tests a race condition, or permission problem, or OS
|
||||
# incompatibility in which, for some reason, no process name can be
|
||||
|
|
@ -103,7 +103,7 @@ class AlreadyListeningTestPsutil(AlreadyListeningTest):
|
|||
|
||||
@mock.patch("certbot.plugins.util.psutil.net_connections")
|
||||
@mock.patch("certbot.plugins.util.psutil.Process")
|
||||
@mock.patch("certbot.plugins.util.zope.component.getUtility")
|
||||
@test_util.patch_get_utility()
|
||||
def test_not_listening(self, mock_get_utility, mock_process, mock_net):
|
||||
from psutil._common import sconn
|
||||
conns = [
|
||||
|
|
@ -121,7 +121,7 @@ class AlreadyListeningTestPsutil(AlreadyListeningTest):
|
|||
|
||||
@mock.patch("certbot.plugins.util.psutil.net_connections")
|
||||
@mock.patch("certbot.plugins.util.psutil.Process")
|
||||
@mock.patch("certbot.plugins.util.zope.component.getUtility")
|
||||
@test_util.patch_get_utility()
|
||||
def test_listening_ipv4(self, mock_get_utility, mock_process, mock_net):
|
||||
from psutil._common import sconn
|
||||
conns = [
|
||||
|
|
@ -142,7 +142,7 @@ class AlreadyListeningTestPsutil(AlreadyListeningTest):
|
|||
|
||||
@mock.patch("certbot.plugins.util.psutil.net_connections")
|
||||
@mock.patch("certbot.plugins.util.psutil.Process")
|
||||
@mock.patch("certbot.plugins.util.zope.component.getUtility")
|
||||
@test_util.patch_get_utility()
|
||||
def test_listening_ipv6(self, mock_get_utility, mock_process, mock_net):
|
||||
from psutil._common import sconn
|
||||
conns = [
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ to serve all files under specified web root ({0})."""
|
|||
"times to handle different domains; each domain will have "
|
||||
"the webroot path that preceded it. For instance: `-w "
|
||||
"/var/www/example -d example.com -d www.example.com -w "
|
||||
"/var/www/thing -d thing.net -d m.thing.net`")
|
||||
"/var/www/thing -d thing.net -d m.thing.net` (default: Ask)")
|
||||
add("map", default={}, action=_WebrootMapAction,
|
||||
help="JSON dictionary mapping domains to webroot paths; this "
|
||||
"implies -d for each entry. You may need to escape this from "
|
||||
|
|
@ -110,12 +110,13 @@ to serve all files under specified web root ({0})."""
|
|||
|
||||
def _prompt_with_webroot_list(self, domain, known_webroots):
|
||||
display = zope.component.getUtility(interfaces.IDisplay)
|
||||
path_flag = "--" + self.option_name("path")
|
||||
|
||||
while True:
|
||||
code, index = display.menu(
|
||||
"Select the webroot for {0}:".format(domain),
|
||||
["Enter a new webroot"] + known_webroots,
|
||||
help_label="Help", cli_flag="--" + self.option_name("path"))
|
||||
help_label="Help", cli_flag=path_flag, force_interactive=True)
|
||||
if code == display_util.CANCEL:
|
||||
raise errors.PluginError(
|
||||
"Every requested domain must have a "
|
||||
|
|
@ -129,7 +130,8 @@ to serve all files under specified web root ({0})."""
|
|||
"public_html or webroot directory. The webroot "
|
||||
"plugin works by temporarily saving necessary "
|
||||
"resources in the HTTP server's webroot directory "
|
||||
"to pass domain validation challenges.")
|
||||
"to pass domain validation challenges.",
|
||||
force_interactive=True)
|
||||
else: # code == display_util.OK
|
||||
return None if index == 0 else known_webroots[index - 1]
|
||||
|
||||
|
|
@ -138,11 +140,11 @@ to serve all files under specified web root ({0})."""
|
|||
|
||||
while True:
|
||||
code, webroot = display.directory_select(
|
||||
"Input the webroot for {0}:".format(domain))
|
||||
"Input the webroot for {0}:".format(domain),
|
||||
force_interactive=True)
|
||||
if code == display_util.HELP:
|
||||
# Help can currently only be selected
|
||||
# when using the ncurses interface
|
||||
display.notification(display_util.DSELECT_HELP)
|
||||
# Displaying help is not currently implemented
|
||||
return None
|
||||
elif code == display_util.CANCEL:
|
||||
return None
|
||||
else: # code == display_util.OK
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ from certbot import errors
|
|||
from certbot.display import util as display_util
|
||||
|
||||
from certbot.tests import acme_util
|
||||
from certbot.tests import test_util
|
||||
from certbot.tests import util as test_util
|
||||
|
||||
|
||||
KEY = jose.JWKRSA.load(test_util.load_vector("rsa512_key.pem"))
|
||||
|
|
@ -61,7 +61,7 @@ class AuthenticatorTest(unittest.TestCase):
|
|||
def test_prepare(self):
|
||||
self.auth.prepare() # shouldn't raise any exceptions
|
||||
|
||||
@mock.patch("certbot.plugins.webroot.zope.component.getUtility")
|
||||
@test_util.patch_get_utility()
|
||||
def test_webroot_from_list(self, mock_get_utility):
|
||||
self.config.webroot_path = []
|
||||
self.config.webroot_map = {"otherthing.com": self.path}
|
||||
|
|
@ -78,7 +78,7 @@ class AuthenticatorTest(unittest.TestCase):
|
|||
self.assertEqual(self.config.webroot_map[self.achall.domain],
|
||||
self.path)
|
||||
|
||||
@mock.patch("certbot.plugins.webroot.zope.component.getUtility")
|
||||
@test_util.patch_get_utility()
|
||||
def test_webroot_from_list_help_and_cancel(self, mock_get_utility):
|
||||
self.config.webroot_path = []
|
||||
self.config.webroot_map = {"otherthing.com": self.path}
|
||||
|
|
@ -95,7 +95,7 @@ class AuthenticatorTest(unittest.TestCase):
|
|||
webroot in call[0][1]
|
||||
for webroot in six.itervalues(self.config.webroot_map)))
|
||||
|
||||
@mock.patch("certbot.plugins.webroot.zope.component.getUtility")
|
||||
@test_util.patch_get_utility()
|
||||
def test_new_webroot(self, mock_get_utility):
|
||||
self.config.webroot_path = []
|
||||
self.config.webroot_map = {}
|
||||
|
|
@ -111,8 +111,7 @@ class AuthenticatorTest(unittest.TestCase):
|
|||
|
||||
self.assertTrue(mock_display.notification.called)
|
||||
for call in mock_display.notification.call_args_list:
|
||||
self.assertTrue(imaginary_dir in call[0][0] or
|
||||
display_util.DSELECT_HELP == call[0][0])
|
||||
self.assertTrue(imaginary_dir in call[0][0])
|
||||
|
||||
self.assertTrue(mock_display.directory_select.called)
|
||||
for call in mock_display.directory_select.call_args_list:
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
"""Functionality for autorenewal and associated juggling of configurations"""
|
||||
from __future__ import print_function
|
||||
import copy
|
||||
import glob
|
||||
import itertools
|
||||
import logging
|
||||
import os
|
||||
import traceback
|
||||
|
|
@ -11,9 +11,7 @@ import zope.component
|
|||
|
||||
import OpenSSL
|
||||
|
||||
from certbot import configuration
|
||||
from certbot import cli
|
||||
from certbot import constants
|
||||
|
||||
from certbot import crypto_util
|
||||
from certbot import errors
|
||||
|
|
@ -31,20 +29,20 @@ logger = logging.getLogger(__name__)
|
|||
# the renewal configuration process loses this information.
|
||||
STR_CONFIG_ITEMS = ["config_dir", "logs_dir", "work_dir", "user_agent",
|
||||
"server", "account", "authenticator", "installer",
|
||||
"standalone_supported_challenges"]
|
||||
"standalone_supported_challenges", "renew_hook",
|
||||
"pre_hook", "post_hook"]
|
||||
INT_CONFIG_ITEMS = ["rsa_key_size", "tls_sni_01_port", "http01_port"]
|
||||
BOOL_CONFIG_ITEMS = ["must_staple", "allow_subset_of_names"]
|
||||
|
||||
|
||||
def renewal_conf_files(config):
|
||||
"""Return /path/to/*.conf in the renewal conf directory"""
|
||||
return glob.glob(os.path.join(config.renewal_configs_dir, "*.conf"))
|
||||
CONFIG_ITEMS = set(itertools.chain(
|
||||
BOOL_CONFIG_ITEMS, INT_CONFIG_ITEMS, STR_CONFIG_ITEMS))
|
||||
|
||||
|
||||
def _reconstitute(config, full_path):
|
||||
"""Try to instantiate a RenewableCert, updating config with relevant items.
|
||||
|
||||
This is specifically for use in renewal and enforces several checks
|
||||
and policies to ensure that we can try to proceed with the renwal
|
||||
and policies to ensure that we can try to proceed with the renewal
|
||||
request. The config argument is modified by including relevant options
|
||||
read from the renewal configuration file.
|
||||
|
||||
|
|
@ -58,8 +56,7 @@ def _reconstitute(config, full_path):
|
|||
|
||||
"""
|
||||
try:
|
||||
renewal_candidate = storage.RenewableCert(
|
||||
full_path, configuration.RenewerConfiguration(config))
|
||||
renewal_candidate = storage.RenewableCert(full_path, config)
|
||||
except (errors.CertStorageError, IOError) as exc:
|
||||
logger.warning(exc)
|
||||
logger.warning("Renewal configuration file %s is broken. Skipping.", full_path)
|
||||
|
|
@ -77,7 +74,7 @@ def _reconstitute(config, full_path):
|
|||
# Now restore specific values along with their data types, if
|
||||
# those elements are present.
|
||||
try:
|
||||
_restore_required_config_elements(config, renewalparams)
|
||||
restore_required_config_elements(config, renewalparams)
|
||||
_restore_plugin_configs(config, renewalparams)
|
||||
except (ValueError, errors.Error) as error:
|
||||
logger.warning(
|
||||
|
|
@ -157,7 +154,7 @@ def _restore_plugin_configs(config, renewalparams):
|
|||
setattr(config.namespace, config_item, cast(config_value))
|
||||
|
||||
|
||||
def _restore_required_config_elements(config, renewalparams):
|
||||
def restore_required_config_elements(config, renewalparams):
|
||||
"""Sets non-plugin specific values in config from renewalparams
|
||||
|
||||
:param configuration.NamespaceConfig config: configuration for the
|
||||
|
|
@ -166,30 +163,69 @@ def _restore_required_config_elements(config, renewalparams):
|
|||
configuration file that defines this lineage
|
||||
|
||||
"""
|
||||
# string-valued items to add if they're present
|
||||
for config_item in STR_CONFIG_ITEMS:
|
||||
if config_item in renewalparams and not cli.set_by_cli(config_item):
|
||||
value = renewalparams[config_item]
|
||||
# Unfortunately, we've lost type information from ConfigObj,
|
||||
# so we don't know if the original was NoneType or str!
|
||||
if value == "None":
|
||||
value = None
|
||||
setattr(config.namespace, config_item, value)
|
||||
# int-valued items to add if they're present
|
||||
for config_item in INT_CONFIG_ITEMS:
|
||||
if config_item in renewalparams and not cli.set_by_cli(config_item):
|
||||
config_value = renewalparams[config_item]
|
||||
# the default value for http01_port was None during private beta
|
||||
if config_item == "http01_port" and config_value == "None":
|
||||
logger.info("updating legacy http01_port value")
|
||||
int_value = cli.flag_default("http01_port")
|
||||
else:
|
||||
try:
|
||||
int_value = int(config_value)
|
||||
except ValueError:
|
||||
raise errors.Error(
|
||||
"Expected a numeric value for {0}".format(config_item))
|
||||
setattr(config.namespace, config_item, int_value)
|
||||
|
||||
required_items = itertools.chain(
|
||||
six.moves.zip(BOOL_CONFIG_ITEMS, itertools.repeat(_restore_bool)),
|
||||
six.moves.zip(INT_CONFIG_ITEMS, itertools.repeat(_restore_int)),
|
||||
six.moves.zip(STR_CONFIG_ITEMS, itertools.repeat(_restore_str)))
|
||||
for item_name, restore_func in required_items:
|
||||
if item_name in renewalparams and not cli.set_by_cli(item_name):
|
||||
value = restore_func(item_name, renewalparams[item_name])
|
||||
setattr(config.namespace, item_name, value)
|
||||
|
||||
|
||||
def _restore_bool(name, value):
|
||||
"""Restores an boolean key-value pair from a renewal config file.
|
||||
|
||||
:param str name: option name
|
||||
:param str value: option value
|
||||
|
||||
:returns: converted option value to be stored in the runtime config
|
||||
:rtype: bool
|
||||
|
||||
:raises errors.Error: if value can't be converted to an bool
|
||||
|
||||
"""
|
||||
lowercase_value = value.lower()
|
||||
if lowercase_value not in ("true", "false"):
|
||||
raise errors.Error(
|
||||
"Expected True or False for {0} but found {1}".format(name, value))
|
||||
return lowercase_value == "true"
|
||||
|
||||
|
||||
def _restore_int(name, value):
|
||||
"""Restores an integer key-value pair from a renewal config file.
|
||||
|
||||
:param str name: option name
|
||||
:param str value: option value
|
||||
|
||||
:returns: converted option value to be stored in the runtime config
|
||||
:rtype: int
|
||||
|
||||
:raises errors.Error: if value can't be converted to an int
|
||||
|
||||
"""
|
||||
if name == "http01_port" and value == "None":
|
||||
logger.info("updating legacy http01_port value")
|
||||
return cli.flag_default("http01_port")
|
||||
|
||||
try:
|
||||
return int(value)
|
||||
except ValueError:
|
||||
raise errors.Error("Expected a numeric value for {0}".format(name))
|
||||
|
||||
|
||||
def _restore_str(unused_name, value):
|
||||
"""Restores an string key-value pair from a renewal config file.
|
||||
|
||||
:param str unused_name: option name
|
||||
:param str value: option value
|
||||
|
||||
:returns: converted option value to be stored in the runtime config
|
||||
:rtype: str or None
|
||||
|
||||
"""
|
||||
return None if value == "None" else value
|
||||
|
||||
|
||||
def should_renew(config, lineage):
|
||||
|
|
@ -209,9 +245,6 @@ def should_renew(config, lineage):
|
|||
|
||||
def _avoid_invalidating_lineage(config, lineage, original_server):
|
||||
"Do not renew a valid cert with one from a staging server!"
|
||||
def _is_staging(srv):
|
||||
return srv == constants.STAGING_URI or "staging" in srv
|
||||
|
||||
# Some lineages may have begun with --staging, but then had production certs
|
||||
# added to them
|
||||
latest_cert = OpenSSL.crypto.load_certificate(
|
||||
|
|
@ -220,8 +253,8 @@ def _avoid_invalidating_lineage(config, lineage, original_server):
|
|||
# we should test more methodically
|
||||
now_valid = "fake" not in repr(latest_cert.get_issuer()).lower()
|
||||
|
||||
if _is_staging(config.server):
|
||||
if not _is_staging(original_server) or now_valid:
|
||||
if util.is_staging(config.server):
|
||||
if not util.is_staging(original_server) or now_valid:
|
||||
if not config.break_my_certs:
|
||||
names = ", ".join(lineage.names())
|
||||
raise errors.Error(
|
||||
|
|
@ -230,12 +263,12 @@ def _avoid_invalidating_lineage(config, lineage, original_server):
|
|||
"unless you use the --break-my-certs flag!".format(names))
|
||||
|
||||
|
||||
def renew_cert(config, domains, le_client, lineage):
|
||||
def renew_cert(config, le_client, lineage):
|
||||
"Renew a certificate lineage."
|
||||
renewal_params = lineage.configuration["renewalparams"]
|
||||
original_server = renewal_params.get("server", cli.flag_default("server"))
|
||||
_avoid_invalidating_lineage(config, lineage, original_server)
|
||||
new_certr, new_chain, new_key, _ = le_client.obtain_certificate(domains)
|
||||
new_certr, new_chain, new_key, _ = le_client.obtain_certificate(lineage.names())
|
||||
if config.dry_run:
|
||||
logger.debug("Dry run: skipping updating lineage at %s",
|
||||
os.path.dirname(lineage.cert))
|
||||
|
|
@ -244,12 +277,11 @@ def renew_cert(config, domains, le_client, lineage):
|
|||
new_cert = OpenSSL.crypto.dump_certificate(
|
||||
OpenSSL.crypto.FILETYPE_PEM, new_certr.body.wrapped)
|
||||
new_chain = crypto_util.dump_pyopenssl_chain(new_chain)
|
||||
renewal_conf = configuration.RenewerConfiguration(config.namespace)
|
||||
# TODO: Check return value of save_successor
|
||||
lineage.save_successor(prior_version, new_cert, new_key.pem, new_chain, renewal_conf)
|
||||
lineage.save_successor(prior_version, new_cert, new_key.pem, new_chain, config)
|
||||
lineage.update_all_links_to(lineage.latest_common_version())
|
||||
|
||||
hooks.renew_hook(config, domains, lineage.live_dir)
|
||||
hooks.renew_hook(config, lineage.names(), lineage.live_dir)
|
||||
|
||||
|
||||
def report(msgs, category):
|
||||
|
|
@ -272,6 +304,9 @@ def _renew_describe_results(config, renew_successes, renew_failures,
|
|||
notify(report(renew_skipped, "skipped"))
|
||||
if not renew_successes and not renew_failures:
|
||||
notify("No renewals were attempted.")
|
||||
if (config.pre_hook is not None or
|
||||
config.renew_hook is not None or config.post_hook is not None):
|
||||
notify("No hooks were run.")
|
||||
elif renew_successes and not renew_failures:
|
||||
notify("Congratulations, all renewals succeeded. The following certs "
|
||||
"have been renewed:")
|
||||
|
|
@ -300,26 +335,31 @@ def _renew_describe_results(config, renew_successes, renew_failures,
|
|||
print("\n".join(out))
|
||||
|
||||
|
||||
def renew_all_lineages(config):
|
||||
def handle_renewal_request(config):
|
||||
"""Examine each lineage; renew if due and report results"""
|
||||
|
||||
# This is trivially False if config.domains is empty
|
||||
if any(domain not in config.webroot_map for domain in config.domains):
|
||||
# If more plugins start using cli.add_domains,
|
||||
# we may want to only log a warning here
|
||||
raise errors.Error("Currently, the renew verb is only capable of "
|
||||
raise errors.Error("Currently, the renew verb is capable of either "
|
||||
"renewing all installed certificates that are due "
|
||||
"to be renewed; individual domains cannot be "
|
||||
"specified with this action. If you would like to "
|
||||
"renew specific certificates, use the certonly "
|
||||
"to be renewed or renewing a single certificate specified "
|
||||
"by its name. If you would like to renew specific "
|
||||
"certificates by their domains, use the certonly "
|
||||
"command. The renew verb may provide other options "
|
||||
"for selecting certificates to renew in the future.")
|
||||
renewer_config = configuration.RenewerConfiguration(config)
|
||||
|
||||
if config.certname:
|
||||
conf_files = [storage.renewal_file_for_certname(config, config.certname)]
|
||||
else:
|
||||
conf_files = storage.renewal_conf_files(config)
|
||||
|
||||
renew_successes = []
|
||||
renew_failures = []
|
||||
renew_skipped = []
|
||||
parse_failures = []
|
||||
for renewal_file in renewal_conf_files(renewer_config):
|
||||
for renewal_file in conf_files:
|
||||
disp = zope.component.getUtility(interfaces.IDisplay)
|
||||
disp.notification("Processing " + renewal_file, pause=False)
|
||||
lineage_config = copy.deepcopy(config)
|
||||
|
|
|
|||
|
|
@ -15,8 +15,6 @@ from certbot import errors
|
|||
from certbot import interfaces
|
||||
from certbot import util
|
||||
|
||||
from certbot.display import util as display_util
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -183,7 +181,7 @@ class Reverter(object):
|
|||
if for_logging:
|
||||
return os.linesep.join(output)
|
||||
zope.component.getUtility(interfaces.IDisplay).notification(
|
||||
os.linesep.join(output), display_util.HEIGHT)
|
||||
os.linesep.join(output), force_interactive=True)
|
||||
|
||||
def add_to_temp_checkpoint(self, save_files, save_notes):
|
||||
"""Add files to temporary checkpoint.
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
"""Renewable certificates storage."""
|
||||
import datetime
|
||||
import glob
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
|
|
@ -7,6 +8,7 @@ import re
|
|||
import configobj
|
||||
import parsedatetime
|
||||
import pytz
|
||||
import shutil
|
||||
import six
|
||||
|
||||
import certbot
|
||||
|
|
@ -20,9 +22,22 @@ from certbot import util
|
|||
logger = logging.getLogger(__name__)
|
||||
|
||||
ALL_FOUR = ("cert", "privkey", "chain", "fullchain")
|
||||
README = "README"
|
||||
CURRENT_VERSION = util.get_strict_version(certbot.__version__)
|
||||
|
||||
|
||||
def renewal_conf_files(config):
|
||||
"""Return /path/to/*.conf in the renewal conf directory"""
|
||||
return glob.glob(os.path.join(config.renewal_configs_dir, "*.conf"))
|
||||
|
||||
def renewal_file_for_certname(config, certname):
|
||||
"""Return /path/to/certname.conf in the renewal conf directory"""
|
||||
path = os.path.join(config.renewal_configs_dir, "{0}.conf".format(certname))
|
||||
if not os.path.exists(path):
|
||||
raise errors.CertStorageError("No certificate found with name {0} (expected "
|
||||
"{1}).".format(certname, path))
|
||||
return path
|
||||
|
||||
def config_with_defaults(config=None):
|
||||
"""Merge supplied config, if provided, on top of builtin defaults."""
|
||||
defaults_copy = configobj.ConfigObj(constants.RENEWER_DEFAULTS)
|
||||
|
|
@ -54,11 +69,12 @@ def add_time_interval(base_time, interval, textparser=parsedatetime.Calendar()):
|
|||
return textparser.parseDT(interval, base_time, tzinfo=tzinfo)[0]
|
||||
|
||||
|
||||
def write_renewal_config(o_filename, n_filename, target, relevant_data):
|
||||
def write_renewal_config(o_filename, n_filename, archive_dir, target, relevant_data):
|
||||
"""Writes a renewal config file with the specified name and values.
|
||||
|
||||
:param str o_filename: Absolute path to the previous version of config file
|
||||
:param str n_filename: Absolute path to the new destination of config file
|
||||
:param str archive_dir: Absolute path to the archive directory
|
||||
:param dict target: Maps ALL_FOUR to their symlink paths
|
||||
:param dict relevant_data: Renewal configuration options to save
|
||||
|
||||
|
|
@ -68,6 +84,7 @@ def write_renewal_config(o_filename, n_filename, target, relevant_data):
|
|||
"""
|
||||
config = configobj.ConfigObj(o_filename)
|
||||
config["version"] = certbot.__version__
|
||||
config["archive_dir"] = archive_dir
|
||||
for kind in ALL_FOUR:
|
||||
config[kind] = target[kind]
|
||||
|
||||
|
|
@ -94,21 +111,38 @@ def write_renewal_config(o_filename, n_filename, target, relevant_data):
|
|||
config.write(outfile=f)
|
||||
return config
|
||||
|
||||
def rename_renewal_config(prev_name, new_name, cli_config):
|
||||
"""Renames cli_config.certname's config to cli_config.new_certname.
|
||||
|
||||
def update_configuration(lineagename, target, cli_config):
|
||||
:param .NamespaceConfig cli_config: parsed command line
|
||||
arguments
|
||||
"""
|
||||
prev_filename = renewal_filename_for_lineagename(cli_config, prev_name)
|
||||
new_filename = renewal_filename_for_lineagename(cli_config, new_name)
|
||||
if os.path.exists(new_filename):
|
||||
raise errors.ConfigurationError("The new certificate name "
|
||||
"is already in use.")
|
||||
try:
|
||||
os.rename(prev_filename, new_filename)
|
||||
except OSError:
|
||||
raise errors.ConfigurationError("Please specify a valid filename "
|
||||
"for the new certificate name.")
|
||||
|
||||
|
||||
def update_configuration(lineagename, archive_dir, target, cli_config):
|
||||
"""Modifies lineagename's config to contain the specified values.
|
||||
|
||||
:param str lineagename: Name of the lineage being modified
|
||||
:param str archive_dir: Absolute path to the archive directory
|
||||
:param dict target: Maps ALL_FOUR to their symlink paths
|
||||
:param .RenewerConfiguration cli_config: parsed command line
|
||||
:param .NamespaceConfig cli_config: parsed command line
|
||||
arguments
|
||||
|
||||
:returns: Configuration object for the updated config file
|
||||
:rtype: configobj.ConfigObj
|
||||
|
||||
"""
|
||||
config_filename = os.path.join(
|
||||
cli_config.renewal_configs_dir, lineagename) + ".conf"
|
||||
config_filename = renewal_filename_for_lineagename(cli_config, lineagename)
|
||||
temp_filename = config_filename + ".new"
|
||||
|
||||
# If an existing tempfile exists, delete it
|
||||
|
|
@ -117,7 +151,7 @@ def update_configuration(lineagename, target, cli_config):
|
|||
|
||||
# Save only the config items that are relevant to renewal
|
||||
values = relevant_values(vars(cli_config.namespace))
|
||||
write_renewal_config(config_filename, temp_filename, target, values)
|
||||
write_renewal_config(config_filename, temp_filename, archive_dir, target, values)
|
||||
os.rename(temp_filename, config_filename)
|
||||
|
||||
return configobj.ConfigObj(config_filename)
|
||||
|
|
@ -149,9 +183,9 @@ def _relevant(option):
|
|||
from certbot import renewal
|
||||
from certbot.plugins import disco as plugins_disco
|
||||
plugins = list(plugins_disco.PluginsRegistry.find_all())
|
||||
return (option in renewal.STR_CONFIG_ITEMS
|
||||
or option in renewal.INT_CONFIG_ITEMS
|
||||
or any(option.startswith(x + "_") for x in plugins))
|
||||
|
||||
return (option in renewal.CONFIG_ITEMS or
|
||||
any(option.startswith(x + "_") for x in plugins))
|
||||
|
||||
|
||||
def relevant_values(all_values):
|
||||
|
|
@ -168,8 +202,109 @@ def relevant_values(all_values):
|
|||
for option, value in six.iteritems(all_values)
|
||||
if _relevant(option) and cli.option_was_set(option, value))
|
||||
|
||||
def lineagename_for_filename(config_filename):
|
||||
"""Returns the lineagename for a configuration filename.
|
||||
"""
|
||||
if not config_filename.endswith(".conf"):
|
||||
raise errors.CertStorageError(
|
||||
"renewal config file name must end in .conf")
|
||||
return os.path.basename(config_filename[:-len(".conf")])
|
||||
|
||||
class RenewableCert(object): # pylint: disable=too-many-instance-attributes
|
||||
def renewal_filename_for_lineagename(config, lineagename):
|
||||
"""Returns the lineagename for a configuration filename.
|
||||
"""
|
||||
return os.path.join(config.renewal_configs_dir, lineagename) + ".conf"
|
||||
|
||||
def _relpath_from_file(archive_dir, from_file):
|
||||
"""Path to a directory from a file"""
|
||||
return os.path.relpath(archive_dir, os.path.dirname(from_file))
|
||||
|
||||
def _full_archive_path(config_obj, cli_config, lineagename):
|
||||
"""Returns the full archive path for a lineagename
|
||||
|
||||
Uses cli_config to determine archive path if not available from config_obj.
|
||||
|
||||
:param configobj.ConfigObj config_obj: Renewal conf file contents (can be None)
|
||||
:param configuration.NamespaceConfig cli_config: Main config file
|
||||
:param str lineagename: Certificate name
|
||||
"""
|
||||
if config_obj and "archive_dir" in config_obj:
|
||||
return config_obj["archive_dir"]
|
||||
else:
|
||||
return os.path.join(cli_config.default_archive_dir, lineagename)
|
||||
|
||||
def _full_live_path(cli_config, lineagename):
|
||||
"""Returns the full default live path for a lineagename"""
|
||||
return os.path.join(cli_config.live_dir, lineagename)
|
||||
|
||||
def delete_files(config, certname):
|
||||
"""Delete all files related to the certificate.
|
||||
|
||||
If some files are not found, ignore them and continue.
|
||||
"""
|
||||
renewal_filename = renewal_file_for_certname(config, certname)
|
||||
# file exists
|
||||
full_default_archive_dir = _full_archive_path(None, config, certname)
|
||||
full_default_live_dir = _full_live_path(config, certname)
|
||||
try:
|
||||
renewal_config = configobj.ConfigObj(renewal_filename)
|
||||
except configobj.ConfigObjError:
|
||||
# config is corrupted
|
||||
logger.warning("Could not parse %s. You may wish to manually "
|
||||
"delete the contents of %s and %s.", renewal_filename,
|
||||
full_default_live_dir, full_default_archive_dir)
|
||||
raise errors.CertStorageError(
|
||||
"error parsing {0}".format(renewal_filename))
|
||||
finally:
|
||||
# we couldn't read it, but let's at least delete it
|
||||
# if this was going to fail, it already would have.
|
||||
os.remove(renewal_filename)
|
||||
logger.debug("Removed %s", renewal_filename)
|
||||
|
||||
# cert files and (hopefully) live directory
|
||||
# it's not guaranteed that the files are in our default storage
|
||||
# structure. so, first delete the cert files.
|
||||
directory_names = set()
|
||||
for kind in ALL_FOUR:
|
||||
link = renewal_config.get(kind)
|
||||
try:
|
||||
os.remove(link)
|
||||
logger.debug("Removed %s", link)
|
||||
except OSError:
|
||||
logger.debug("Unable to delete %s", link)
|
||||
directory = os.path.dirname(link)
|
||||
directory_names.add(directory)
|
||||
|
||||
# if all four were in the same directory, and the only thing left
|
||||
# is the README file (or nothing), delete that directory.
|
||||
# this will be wrong in very few but some cases.
|
||||
if len(directory_names) == 1:
|
||||
# delete the README file
|
||||
directory = directory_names.pop()
|
||||
readme_path = os.path.join(directory, README)
|
||||
try:
|
||||
os.remove(readme_path)
|
||||
logger.debug("Removed %s", readme_path)
|
||||
except OSError:
|
||||
logger.debug("Unable to delete %s", readme_path)
|
||||
# if it's now empty, delete the directory
|
||||
try:
|
||||
os.rmdir(directory) # only removes empty directories
|
||||
logger.debug("Removed %s", directory)
|
||||
except OSError:
|
||||
logger.debug("Unable to remove %s; may not be empty.", directory)
|
||||
|
||||
# archive directory
|
||||
try:
|
||||
archive_path = _full_archive_path(renewal_config, config, certname)
|
||||
shutil.rmtree(archive_path)
|
||||
logger.debug("Removed %s", archive_path)
|
||||
except OSError:
|
||||
logger.debug("Unable to remove %s", archive_path)
|
||||
|
||||
|
||||
class RenewableCert(object):
|
||||
# pylint: disable=too-many-instance-attributes,too-many-public-methods
|
||||
"""Renewable certificate.
|
||||
|
||||
Represents a lineage of certificates that is under the management of
|
||||
|
|
@ -204,23 +339,19 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes
|
|||
renewal configuration file and/or systemwide defaults.
|
||||
|
||||
"""
|
||||
def __init__(self, config_filename, cli_config):
|
||||
def __init__(self, config_filename, cli_config, update_symlinks=False):
|
||||
"""Instantiate a RenewableCert object from an existing lineage.
|
||||
|
||||
:param str config_filename: the path to the renewal config file
|
||||
that defines this lineage.
|
||||
:param .RenewerConfiguration: parsed command line arguments
|
||||
:param .NamespaceConfig: parsed command line arguments
|
||||
|
||||
:raises .CertStorageError: if the configuration file's name didn't end
|
||||
in ".conf", or the file is missing or broken.
|
||||
|
||||
"""
|
||||
self.cli_config = cli_config
|
||||
if not config_filename.endswith(".conf"):
|
||||
raise errors.CertStorageError(
|
||||
"renewal config file name must end in .conf")
|
||||
self.lineagename = os.path.basename(
|
||||
config_filename[:-len(".conf")])
|
||||
self.lineagename = lineagename_for_filename(config_filename)
|
||||
|
||||
# self.configuration should be used to read parameters that
|
||||
# may have been chosen based on default values from the
|
||||
|
|
@ -256,8 +387,41 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes
|
|||
self.live_dir = os.path.dirname(self.cert)
|
||||
|
||||
self._fix_symlinks()
|
||||
if update_symlinks:
|
||||
self._update_symlinks()
|
||||
self._check_symlinks()
|
||||
|
||||
@property
|
||||
def target_expiry(self):
|
||||
"""The current target certificate's expiration datetime
|
||||
|
||||
:returns: Expiration datetime of the current target certificate
|
||||
:rtype: :class:`datetime.datetime`
|
||||
"""
|
||||
return crypto_util.notAfter(self.current_target("cert"))
|
||||
|
||||
@property
|
||||
def archive_dir(self):
|
||||
"""Returns the default or specified archive directory"""
|
||||
return _full_archive_path(self.configuration,
|
||||
self.cli_config, self.lineagename)
|
||||
|
||||
def relative_archive_dir(self, from_file):
|
||||
"""Returns the default or specified archive directory as a relative path
|
||||
|
||||
Used for creating symbolic links.
|
||||
"""
|
||||
return _relpath_from_file(self.archive_dir, from_file)
|
||||
|
||||
@property
|
||||
def is_test_cert(self):
|
||||
"""Returns true if this is a test cert from a staging server."""
|
||||
server = self.configuration["renewalparams"].get("server", None)
|
||||
if server:
|
||||
return util.is_staging(server)
|
||||
else:
|
||||
return False
|
||||
|
||||
def _check_symlinks(self):
|
||||
"""Raises an exception if a symlink doesn't exist"""
|
||||
for kind in ALL_FOUR:
|
||||
|
|
@ -270,6 +434,17 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes
|
|||
raise errors.CertStorageError("target {0} of symlink {1} does "
|
||||
"not exist".format(target, link))
|
||||
|
||||
def _update_symlinks(self):
|
||||
"""Updates symlinks to use archive_dir"""
|
||||
for kind in ALL_FOUR:
|
||||
link = getattr(self, kind)
|
||||
previous_link = get_link_target(link)
|
||||
new_link = os.path.join(self.relative_archive_dir(link),
|
||||
os.path.basename(previous_link))
|
||||
|
||||
os.unlink(link)
|
||||
os.symlink(new_link, link)
|
||||
|
||||
def _consistent(self):
|
||||
"""Are the files associated with this lineage self-consistent?
|
||||
|
||||
|
|
@ -297,16 +472,16 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes
|
|||
|
||||
# Each element's link must point within the cert lineage's
|
||||
# directory within the official archive directory
|
||||
desired_directory = os.path.join(
|
||||
self.cli_config.archive_dir, self.lineagename)
|
||||
if not os.path.samefile(os.path.dirname(target),
|
||||
desired_directory):
|
||||
if not os.path.samefile(os.path.dirname(target), self.archive_dir):
|
||||
logger.debug("Element's link does not point within the "
|
||||
"cert lineage's directory within the "
|
||||
"official archive directory. Link: %s, "
|
||||
"target directory: %s, "
|
||||
"archive directory: %s.",
|
||||
link, os.path.dirname(target), desired_directory)
|
||||
"archive directory: %s. If you've specified "
|
||||
"the archive directory in the renewal configuration "
|
||||
"file, you may need to update links by running "
|
||||
"certbot update_symlinks.",
|
||||
link, os.path.dirname(target), self.archive_dir)
|
||||
return False
|
||||
|
||||
# The link must point to a file that exists
|
||||
|
|
@ -647,9 +822,8 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes
|
|||
if self.has_pending_deployment():
|
||||
interval = self.configuration.get("deploy_before_expiry",
|
||||
"5 days")
|
||||
expiry = crypto_util.notAfter(self.current_target("cert"))
|
||||
now = pytz.UTC.fromutc(datetime.datetime.utcnow())
|
||||
if expiry < add_time_interval(now, interval):
|
||||
if self.target_expiry < add_time_interval(now, interval):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
|
@ -750,7 +924,7 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes
|
|||
:param str cert: the initial certificate version in PEM format
|
||||
:param str privkey: the private key in PEM format
|
||||
:param str chain: the certificate chain in PEM format
|
||||
:param .RenewerConfiguration cli_config: parsed command line
|
||||
:param .NamespaceConfig cli_config: parsed command line
|
||||
arguments
|
||||
|
||||
:returns: the newly-created RenewalCert object
|
||||
|
|
@ -759,23 +933,20 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes
|
|||
"""
|
||||
|
||||
# Examine the configuration and find the new lineage's name
|
||||
for i in (cli_config.renewal_configs_dir, cli_config.archive_dir,
|
||||
for i in (cli_config.renewal_configs_dir, cli_config.default_archive_dir,
|
||||
cli_config.live_dir):
|
||||
if not os.path.exists(i):
|
||||
os.makedirs(i, 0o700)
|
||||
logger.debug("Creating directory %s.", i)
|
||||
config_file, config_filename = util.unique_lineage_name(
|
||||
cli_config.renewal_configs_dir, lineagename)
|
||||
if not config_filename.endswith(".conf"):
|
||||
raise errors.CertStorageError(
|
||||
"renewal config file name must end in .conf")
|
||||
|
||||
# Determine where on disk everything will go
|
||||
# lineagename will now potentially be modified based on which
|
||||
# renewal configuration file could actually be created
|
||||
lineagename = os.path.basename(config_filename)[:-len(".conf")]
|
||||
archive = os.path.join(cli_config.archive_dir, lineagename)
|
||||
live_dir = os.path.join(cli_config.live_dir, lineagename)
|
||||
lineagename = lineagename_for_filename(config_filename)
|
||||
archive = _full_archive_path(None, cli_config, lineagename)
|
||||
live_dir = _full_live_path(cli_config, lineagename)
|
||||
if os.path.exists(archive):
|
||||
raise errors.CertStorageError(
|
||||
"archive directory exists for " + lineagename)
|
||||
|
|
@ -786,37 +957,52 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes
|
|||
os.mkdir(live_dir)
|
||||
logger.debug("Archive directory %s and live "
|
||||
"directory %s created.", archive, live_dir)
|
||||
relative_archive = os.path.join("..", "..", "archive", lineagename)
|
||||
|
||||
# Put the data into the appropriate files on disk
|
||||
target = dict([(kind, os.path.join(live_dir, kind + ".pem"))
|
||||
for kind in ALL_FOUR])
|
||||
for kind in ALL_FOUR:
|
||||
os.symlink(os.path.join(relative_archive, kind + "1.pem"),
|
||||
os.symlink(os.path.join(_relpath_from_file(archive, target[kind]), kind + "1.pem"),
|
||||
target[kind])
|
||||
with open(target["cert"], "w") as f:
|
||||
with open(target["cert"], "wb") as f:
|
||||
logger.debug("Writing certificate to %s.", target["cert"])
|
||||
f.write(cert)
|
||||
with open(target["privkey"], "w") as f:
|
||||
with open(target["privkey"], "wb") as f:
|
||||
logger.debug("Writing private key to %s.", target["privkey"])
|
||||
f.write(privkey)
|
||||
# XXX: Let's make sure to get the file permissions right here
|
||||
with open(target["chain"], "w") as f:
|
||||
with open(target["chain"], "wb") as f:
|
||||
logger.debug("Writing chain to %s.", target["chain"])
|
||||
f.write(chain)
|
||||
with open(target["fullchain"], "w") as f:
|
||||
with open(target["fullchain"], "wb") as f:
|
||||
# assumes that OpenSSL.crypto.dump_certificate includes
|
||||
# ending newline character
|
||||
logger.debug("Writing full chain to %s.", target["fullchain"])
|
||||
f.write(cert + chain)
|
||||
|
||||
# Write a README file to the live directory
|
||||
readme_path = os.path.join(live_dir, README)
|
||||
with open(readme_path, "w") as f:
|
||||
logger.debug("Writing README to %s.", readme_path)
|
||||
f.write("This directory contains your keys and certificates.\n\n"
|
||||
"`privkey.pem` : the private key for your certificate.\n"
|
||||
"`fullchain.pem`: the certificate file used in most server software.\n"
|
||||
"`chain.pem` : used for OCSP stapling in Nginx >=1.3.7.\n"
|
||||
"`cert.pem` : will break many server configurations, and "
|
||||
"should not be used\n"
|
||||
" without reading further documentation (see link below).\n\n"
|
||||
"We recommend not moving these files. For more information, see the Certbot\n"
|
||||
"User Guide at https://certbot.eff.org/docs/using.html#where-are-my-"
|
||||
"certificates.\n")
|
||||
|
||||
# Document what we've done in a new renewal config file
|
||||
config_file.close()
|
||||
|
||||
# Save only the config items that are relevant to renewal
|
||||
values = relevant_values(vars(cli_config.namespace))
|
||||
|
||||
new_config = write_renewal_config(config_filename, config_filename, target, values)
|
||||
new_config = write_renewal_config(config_filename, config_filename, archive,
|
||||
target, values)
|
||||
return cls(new_config.filename, cli_config)
|
||||
|
||||
def save_successor(self, prior_version, new_cert,
|
||||
|
|
@ -836,7 +1022,7 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes
|
|||
:param str new_privkey: the new private key, in PEM format,
|
||||
or ``None``, if the private key has not changed
|
||||
:param str new_chain: the new chain, in PEM format
|
||||
:param .RenewerConfiguration cli_config: parsed command line
|
||||
:param .NamespaceConfig cli_config: parsed command line
|
||||
arguments
|
||||
|
||||
:returns: the new version number that was created
|
||||
|
|
@ -851,14 +1037,9 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes
|
|||
|
||||
self.cli_config = cli_config
|
||||
target_version = self.next_free_version()
|
||||
archive = self.cli_config.archive_dir
|
||||
# XXX if anyone ever moves a renewal configuration file, this will
|
||||
# break... perhaps prefix should be the dirname of the previous
|
||||
# cert.pem?
|
||||
prefix = os.path.join(archive, self.lineagename)
|
||||
target = dict(
|
||||
[(kind,
|
||||
os.path.join(prefix, "{0}{1}.pem".format(kind, target_version)))
|
||||
os.path.join(self.archive_dir, "{0}{1}.pem".format(kind, target_version)))
|
||||
for kind in ALL_FOUR])
|
||||
|
||||
# Distinguish the cases where the privkey has changed and where it
|
||||
|
|
@ -868,7 +1049,7 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes
|
|||
# The behavior below keeps the prior key by creating a new
|
||||
# symlink to the old key or the target of the old key symlink.
|
||||
old_privkey = os.path.join(
|
||||
prefix, "privkey{0}.pem".format(prior_version))
|
||||
self.archive_dir, "privkey{0}.pem".format(prior_version))
|
||||
if os.path.islink(old_privkey):
|
||||
old_privkey = os.readlink(old_privkey)
|
||||
else:
|
||||
|
|
@ -894,7 +1075,7 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes
|
|||
symlinks = dict((kind, self.configuration[kind]) for kind in ALL_FOUR)
|
||||
# Update renewal config file
|
||||
self.configfile = update_configuration(
|
||||
self.lineagename, symlinks, cli_config)
|
||||
self.lineagename, self.archive_dir, symlinks, cli_config)
|
||||
self.configuration = config_with_defaults(self.configfile)
|
||||
|
||||
return target_version
|
||||
|
|
|
|||
|
|
@ -14,10 +14,10 @@ from acme import messages
|
|||
|
||||
from certbot import errors
|
||||
|
||||
from certbot.tests import test_util
|
||||
from certbot.tests import util
|
||||
|
||||
|
||||
KEY = jose.JWKRSA.load(test_util.load_vector("rsa512_key_2.pem"))
|
||||
KEY = jose.JWKRSA.load(util.load_vector("rsa512_key_2.pem"))
|
||||
|
||||
|
||||
class AccountTest(unittest.TestCase):
|
||||
|
|
|
|||
|
|
@ -7,10 +7,13 @@ from acme import challenges
|
|||
from acme import jose
|
||||
from acme import messages
|
||||
|
||||
from certbot.tests import test_util
|
||||
from certbot import auth_handler
|
||||
|
||||
from certbot.tests import util
|
||||
|
||||
|
||||
KEY = test_util.load_rsa_private_key('rsa512_key.pem')
|
||||
JWK = jose.JWK.load(util.load_vector('rsa512_key.pem'))
|
||||
KEY = util.load_rsa_private_key('rsa512_key.pem')
|
||||
|
||||
# Challenges
|
||||
HTTP01 = challenges.HTTP01(
|
||||
|
|
@ -50,6 +53,14 @@ DNS01_P = chall_to_challb(DNS01, messages.STATUS_PENDING)
|
|||
CHALLENGES_P = [HTTP01_P, TLSSNI01_P, DNS01_P]
|
||||
|
||||
|
||||
# AnnotatedChallenge objects
|
||||
HTTP01_A = auth_handler.challb_to_achall(HTTP01_P, JWK, "example.com")
|
||||
TLSSNI01_A = auth_handler.challb_to_achall(TLSSNI01_P, JWK, "example.net")
|
||||
DNS01_A = auth_handler.challb_to_achall(DNS01_P, JWK, "example.org")
|
||||
|
||||
ACHALLENGES = [HTTP01_A, TLSSNI01_A, DNS01_A]
|
||||
|
||||
|
||||
def gen_authzr(authz_status, domain, challs, statuses, combos=True):
|
||||
"""Generate an authorization resource.
|
||||
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ from certbot import errors
|
|||
from certbot import util
|
||||
|
||||
from certbot.tests import acme_util
|
||||
from certbot.tests import util as test_util
|
||||
|
||||
|
||||
class ChallengeFactoryTest(unittest.TestCase):
|
||||
|
|
@ -251,7 +252,7 @@ class PollChallengesTest(unittest.TestCase):
|
|||
self.assertEqual(authzr.body.status, messages.STATUS_PENDING)
|
||||
|
||||
@mock.patch("certbot.auth_handler.time")
|
||||
@mock.patch("certbot.auth_handler.zope.component.getUtility")
|
||||
@test_util.patch_get_utility()
|
||||
def test_poll_challenges_failure(self, unused_mock_time, unused_mock_zope):
|
||||
self.mock_net.poll.side_effect = self._mock_poll_solve_one_invalid
|
||||
self.assertRaises(
|
||||
|
|
@ -412,7 +413,7 @@ class ReportFailedChallsTest(unittest.TestCase):
|
|||
domain="foo.bar",
|
||||
account_key="key")
|
||||
|
||||
@mock.patch("certbot.auth_handler.zope.component.getUtility")
|
||||
@test_util.patch_get_utility()
|
||||
def test_same_error_and_domain(self, mock_zope):
|
||||
from certbot import auth_handler
|
||||
|
||||
|
|
@ -421,7 +422,7 @@ class ReportFailedChallsTest(unittest.TestCase):
|
|||
self.assertTrue(len(call_list) == 1)
|
||||
self.assertTrue("Domain: example.com\nType: tls\nDetail: detail" in call_list[0][0][0])
|
||||
|
||||
@mock.patch("certbot.auth_handler.zope.component.getUtility")
|
||||
@test_util.patch_get_utility()
|
||||
def test_different_errors_and_domains(self, mock_zope):
|
||||
from certbot import auth_handler
|
||||
|
||||
|
|
|
|||
472
certbot/tests/cert_manager_test.py
Normal file
472
certbot/tests/cert_manager_test.py
Normal file
|
|
@ -0,0 +1,472 @@
|
|||
"""Tests for certbot.cert_manager."""
|
||||
# pylint: disable=protected-access
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
import configobj
|
||||
import mock
|
||||
|
||||
from certbot import configuration
|
||||
from certbot import errors
|
||||
|
||||
from certbot.display import util as display_util
|
||||
from certbot.storage import ALL_FOUR
|
||||
|
||||
from certbot.tests import storage_test
|
||||
from certbot.tests import util as test_util
|
||||
|
||||
class BaseCertManagerTest(unittest.TestCase):
|
||||
"""Base class for setting up Cert Manager tests.
|
||||
"""
|
||||
def setUp(self):
|
||||
self.tempdir = tempfile.mkdtemp()
|
||||
|
||||
os.makedirs(os.path.join(self.tempdir, "renewal"))
|
||||
|
||||
self.cli_config = configuration.NamespaceConfig(mock.MagicMock(
|
||||
config_dir=self.tempdir,
|
||||
work_dir=self.tempdir,
|
||||
logs_dir=self.tempdir,
|
||||
quiet=False,
|
||||
))
|
||||
|
||||
self.domains = {
|
||||
"example.org": None,
|
||||
"other.com": os.path.join(self.tempdir, "specialarchive")
|
||||
}
|
||||
self.configs = dict((domain, self._set_up_config(domain, self.domains[domain]))
|
||||
for domain in self.domains)
|
||||
|
||||
# We also create a file that isn't a renewal config in the same
|
||||
# location to test that logic that reads in all-and-only renewal
|
||||
# configs will ignore it and NOT attempt to parse it.
|
||||
junk = open(os.path.join(self.tempdir, "renewal", "IGNORE.THIS"), "w")
|
||||
junk.write("This file should be ignored!")
|
||||
junk.close()
|
||||
|
||||
def _set_up_config(self, domain, custom_archive):
|
||||
# TODO: maybe provide NamespaceConfig.make_dirs?
|
||||
# TODO: main() should create those dirs, c.f. #902
|
||||
os.makedirs(os.path.join(self.tempdir, "live", domain))
|
||||
config = configobj.ConfigObj()
|
||||
|
||||
if custom_archive is not None:
|
||||
os.makedirs(custom_archive)
|
||||
config["archive_dir"] = custom_archive
|
||||
else:
|
||||
os.makedirs(os.path.join(self.tempdir, "archive", domain))
|
||||
|
||||
for kind in ALL_FOUR:
|
||||
config[kind] = os.path.join(self.tempdir, "live", domain,
|
||||
kind + ".pem")
|
||||
|
||||
config.filename = os.path.join(self.tempdir, "renewal",
|
||||
domain + ".conf")
|
||||
config.write()
|
||||
return config
|
||||
|
||||
def tearDown(self):
|
||||
shutil.rmtree(self.tempdir)
|
||||
|
||||
|
||||
class UpdateLiveSymlinksTest(BaseCertManagerTest):
|
||||
"""Tests for certbot.cert_manager.update_live_symlinks
|
||||
"""
|
||||
def test_update_live_symlinks(self):
|
||||
"""Test update_live_symlinks"""
|
||||
# pylint: disable=too-many-statements
|
||||
# create files with incorrect symlinks
|
||||
from certbot import cert_manager
|
||||
archive_paths = {}
|
||||
for domain in self.domains:
|
||||
custom_archive = self.domains[domain]
|
||||
if custom_archive is not None:
|
||||
archive_dir_path = custom_archive
|
||||
else:
|
||||
archive_dir_path = os.path.join(self.tempdir, "archive", domain)
|
||||
archive_paths[domain] = dict((kind,
|
||||
os.path.join(archive_dir_path, kind + "1.pem")) for kind in ALL_FOUR)
|
||||
for kind in ALL_FOUR:
|
||||
live_path = self.configs[domain][kind]
|
||||
archive_path = archive_paths[domain][kind]
|
||||
open(archive_path, 'a').close()
|
||||
# path is incorrect but base must be correct
|
||||
os.symlink(os.path.join(self.tempdir, kind + "1.pem"), live_path)
|
||||
|
||||
# run update symlinks
|
||||
cert_manager.update_live_symlinks(self.cli_config)
|
||||
|
||||
# check that symlinks go where they should
|
||||
prev_dir = os.getcwd()
|
||||
try:
|
||||
for domain in self.domains:
|
||||
for kind in ALL_FOUR:
|
||||
os.chdir(os.path.dirname(self.configs[domain][kind]))
|
||||
self.assertEqual(
|
||||
os.path.realpath(os.readlink(self.configs[domain][kind])),
|
||||
os.path.realpath(archive_paths[domain][kind]))
|
||||
finally:
|
||||
os.chdir(prev_dir)
|
||||
|
||||
|
||||
class DeleteTest(storage_test.BaseRenewableCertTest):
|
||||
"""Tests for certbot.cert_manager.delete
|
||||
"""
|
||||
@test_util.patch_get_utility()
|
||||
@mock.patch('certbot.cert_manager.lineage_for_certname')
|
||||
@mock.patch('certbot.storage.delete_files')
|
||||
def test_delete(self, mock_delete_files, mock_lineage_for_certname, unused_get_utility):
|
||||
"""Test delete"""
|
||||
mock_lineage_for_certname.return_value = self.test_rc
|
||||
self.cli_config.certname = "example.org"
|
||||
from certbot import cert_manager
|
||||
cert_manager.delete(self.cli_config)
|
||||
self.assertTrue(mock_delete_files.called)
|
||||
|
||||
|
||||
class CertificatesTest(BaseCertManagerTest):
|
||||
"""Tests for certbot.cert_manager.certificates
|
||||
"""
|
||||
def _certificates(self, *args, **kwargs):
|
||||
from certbot.cert_manager import certificates
|
||||
return certificates(*args, **kwargs)
|
||||
|
||||
@mock.patch('certbot.cert_manager.logger')
|
||||
@test_util.patch_get_utility()
|
||||
def test_certificates_parse_fail(self, mock_utility, mock_logger):
|
||||
self._certificates(self.cli_config)
|
||||
self.assertTrue(mock_logger.warning.called) #pylint: disable=no-member
|
||||
self.assertTrue(mock_utility.called)
|
||||
|
||||
@mock.patch('certbot.cert_manager.logger')
|
||||
@test_util.patch_get_utility()
|
||||
def test_certificates_quiet(self, mock_utility, mock_logger):
|
||||
self.cli_config.quiet = True
|
||||
self._certificates(self.cli_config)
|
||||
self.assertFalse(mock_utility.notification.called)
|
||||
self.assertTrue(mock_logger.warning.called) #pylint: disable=no-member
|
||||
|
||||
@mock.patch('certbot.cert_manager.logger')
|
||||
@test_util.patch_get_utility()
|
||||
@mock.patch("certbot.storage.RenewableCert")
|
||||
@mock.patch('certbot.cert_manager._report_human_readable')
|
||||
def test_certificates_parse_success(self, mock_report, mock_renewable_cert,
|
||||
mock_utility, mock_logger):
|
||||
mock_report.return_value = ""
|
||||
self._certificates(self.cli_config)
|
||||
self.assertFalse(mock_logger.warning.called) #pylint: disable=no-member
|
||||
self.assertTrue(mock_report.called)
|
||||
self.assertTrue(mock_utility.called)
|
||||
self.assertTrue(mock_renewable_cert.called)
|
||||
|
||||
@mock.patch('certbot.cert_manager.logger')
|
||||
@test_util.patch_get_utility()
|
||||
def test_certificates_no_files(self, mock_utility, mock_logger):
|
||||
tempdir = tempfile.mkdtemp()
|
||||
|
||||
cli_config = configuration.NamespaceConfig(mock.MagicMock(
|
||||
config_dir=tempdir,
|
||||
work_dir=tempdir,
|
||||
logs_dir=tempdir,
|
||||
quiet=False,
|
||||
))
|
||||
|
||||
os.makedirs(os.path.join(tempdir, "renewal"))
|
||||
self._certificates(cli_config)
|
||||
self.assertFalse(mock_logger.warning.called) #pylint: disable=no-member
|
||||
self.assertTrue(mock_utility.called)
|
||||
shutil.rmtree(tempdir)
|
||||
|
||||
@mock.patch('certbot.cert_manager.ocsp.RevocationChecker.ocsp_revoked')
|
||||
def test_report_human_readable(self, mock_revoked):
|
||||
mock_revoked.return_value = None
|
||||
from certbot import cert_manager
|
||||
import datetime, pytz
|
||||
expiry = pytz.UTC.fromutc(datetime.datetime.utcnow())
|
||||
|
||||
cert = mock.MagicMock(lineagename="nameone")
|
||||
cert.target_expiry = expiry
|
||||
cert.names.return_value = ["nameone", "nametwo"]
|
||||
cert.is_test_cert = False
|
||||
parsed_certs = [cert]
|
||||
|
||||
# pylint: disable=protected-access
|
||||
get_report = lambda: cert_manager._report_human_readable(mock_config, parsed_certs)
|
||||
|
||||
mock_config = mock.MagicMock(certname=None, lineagename=None)
|
||||
# pylint: disable=protected-access
|
||||
out = get_report()
|
||||
self.assertTrue("INVALID: EXPIRED" in out)
|
||||
|
||||
cert.target_expiry += datetime.timedelta(hours=2)
|
||||
# pylint: disable=protected-access
|
||||
out = get_report()
|
||||
self.assertTrue('1 hour(s)' in out)
|
||||
self.assertTrue('VALID' in out and not 'INVALID' in out)
|
||||
|
||||
cert.target_expiry += datetime.timedelta(days=1)
|
||||
# pylint: disable=protected-access
|
||||
out = get_report()
|
||||
self.assertTrue('1 day' in out)
|
||||
self.assertFalse('under' in out)
|
||||
self.assertTrue('VALID' in out and not 'INVALID' in out)
|
||||
|
||||
cert.target_expiry += datetime.timedelta(days=2)
|
||||
# pylint: disable=protected-access
|
||||
out = get_report()
|
||||
self.assertTrue('3 days' in out)
|
||||
self.assertTrue('VALID' in out and not 'INVALID' in out)
|
||||
|
||||
cert.is_test_cert = True
|
||||
mock_revoked.return_value = True
|
||||
out = get_report()
|
||||
self.assertTrue('INVALID: TEST_CERT, REVOKED' in out)
|
||||
|
||||
cert = mock.MagicMock(lineagename="indescribable")
|
||||
cert.target_expiry = expiry
|
||||
cert.names.return_value = ["nameone", "thrice.named"]
|
||||
cert.is_test_cert = True
|
||||
parsed_certs.append(cert)
|
||||
|
||||
out = get_report()
|
||||
self.assertEqual(len(re.findall("INVALID:", out)), 2)
|
||||
mock_config.domains = ["thrice.named"]
|
||||
out = get_report()
|
||||
self.assertEqual(len(re.findall("INVALID:", out)), 1)
|
||||
mock_config.domains = ["nameone"]
|
||||
out = get_report()
|
||||
self.assertEqual(len(re.findall("INVALID:", out)), 2)
|
||||
mock_config.certname = "indescribable"
|
||||
out = get_report()
|
||||
self.assertEqual(len(re.findall("INVALID:", out)), 1)
|
||||
mock_config.certname = "horror"
|
||||
out = get_report()
|
||||
self.assertEqual(len(re.findall("INVALID:", out)), 0)
|
||||
|
||||
|
||||
class SearchLineagesTest(BaseCertManagerTest):
|
||||
"""Tests for certbot.cert_manager._search_lineages."""
|
||||
|
||||
@mock.patch('certbot.util.make_or_verify_dir')
|
||||
@mock.patch('certbot.storage.renewal_conf_files')
|
||||
@mock.patch('certbot.storage.RenewableCert')
|
||||
def test_cert_storage_error(self, mock_renewable_cert, mock_renewal_conf_files,
|
||||
mock_make_or_verify_dir):
|
||||
mock_renewal_conf_files.return_value = ["badfile"]
|
||||
mock_renewable_cert.side_effect = errors.CertStorageError
|
||||
from certbot import cert_manager
|
||||
# pylint: disable=protected-access
|
||||
self.assertEqual(cert_manager._search_lineages(self.cli_config, lambda x: x, "check"),
|
||||
"check")
|
||||
self.assertTrue(mock_make_or_verify_dir.called)
|
||||
|
||||
|
||||
class LineageForCertnameTest(BaseCertManagerTest):
|
||||
"""Tests for certbot.cert_manager.lineage_for_certname"""
|
||||
|
||||
@mock.patch('certbot.util.make_or_verify_dir')
|
||||
@mock.patch('certbot.storage.renewal_conf_files')
|
||||
@mock.patch('certbot.storage.RenewableCert')
|
||||
def test_found_match(self, mock_renewable_cert, mock_renewal_conf_files,
|
||||
mock_make_or_verify_dir):
|
||||
mock_renewal_conf_files.return_value = ["somefile.conf"]
|
||||
mock_match = mock.Mock(lineagename="example.com")
|
||||
mock_renewable_cert.return_value = mock_match
|
||||
from certbot import cert_manager
|
||||
self.assertEqual(cert_manager.lineage_for_certname(self.cli_config, "example.com"),
|
||||
mock_match)
|
||||
self.assertTrue(mock_make_or_verify_dir.called)
|
||||
|
||||
@mock.patch('certbot.util.make_or_verify_dir')
|
||||
@mock.patch('certbot.storage.renewal_conf_files')
|
||||
@mock.patch('certbot.storage.RenewableCert')
|
||||
def test_no_match(self, mock_renewable_cert, mock_renewal_conf_files,
|
||||
mock_make_or_verify_dir):
|
||||
mock_renewal_conf_files.return_value = ["somefile.conf"]
|
||||
mock_match = mock.Mock(lineagename="other.com")
|
||||
mock_renewable_cert.return_value = mock_match
|
||||
from certbot import cert_manager
|
||||
self.assertEqual(cert_manager.lineage_for_certname(self.cli_config, "example.com"),
|
||||
None)
|
||||
self.assertTrue(mock_make_or_verify_dir.called)
|
||||
|
||||
|
||||
class DomainsForCertnameTest(BaseCertManagerTest):
|
||||
"""Tests for certbot.cert_manager.domains_for_certname"""
|
||||
|
||||
@mock.patch('certbot.util.make_or_verify_dir')
|
||||
@mock.patch('certbot.storage.renewal_conf_files')
|
||||
@mock.patch('certbot.storage.RenewableCert')
|
||||
def test_found_match(self, mock_renewable_cert, mock_renewal_conf_files,
|
||||
mock_make_or_verify_dir):
|
||||
mock_renewal_conf_files.return_value = ["somefile.conf"]
|
||||
mock_match = mock.Mock(lineagename="example.com")
|
||||
domains = ["example.com", "example.org"]
|
||||
mock_match.names.return_value = domains
|
||||
mock_renewable_cert.return_value = mock_match
|
||||
from certbot import cert_manager
|
||||
self.assertEqual(cert_manager.domains_for_certname(self.cli_config, "example.com"),
|
||||
domains)
|
||||
self.assertTrue(mock_make_or_verify_dir.called)
|
||||
|
||||
@mock.patch('certbot.util.make_or_verify_dir')
|
||||
@mock.patch('certbot.storage.renewal_conf_files')
|
||||
@mock.patch('certbot.storage.RenewableCert')
|
||||
def test_no_match(self, mock_renewable_cert, mock_renewal_conf_files,
|
||||
mock_make_or_verify_dir):
|
||||
mock_renewal_conf_files.return_value = ["somefile.conf"]
|
||||
mock_match = mock.Mock(lineagename="example.com")
|
||||
domains = ["example.com", "example.org"]
|
||||
mock_match.names.return_value = domains
|
||||
mock_renewable_cert.return_value = mock_match
|
||||
from certbot import cert_manager
|
||||
self.assertEqual(cert_manager.domains_for_certname(self.cli_config, "other.com"),
|
||||
None)
|
||||
self.assertTrue(mock_make_or_verify_dir.called)
|
||||
|
||||
|
||||
class RenameLineageTest(BaseCertManagerTest):
|
||||
"""Tests for certbot.cert_manager.rename_lineage"""
|
||||
|
||||
def setUp(self):
|
||||
super(RenameLineageTest, self).setUp()
|
||||
self.mock_config = configuration.NamespaceConfig(
|
||||
namespace=mock.MagicMock(
|
||||
config_dir=self.tempdir,
|
||||
work_dir=self.tempdir,
|
||||
logs_dir=self.tempdir,
|
||||
certname="example.org",
|
||||
new_certname="after",
|
||||
)
|
||||
)
|
||||
|
||||
def _call(self, *args, **kwargs):
|
||||
from certbot import cert_manager
|
||||
return cert_manager.rename_lineage(*args, **kwargs)
|
||||
|
||||
@mock.patch('certbot.storage.renewal_conf_files')
|
||||
@test_util.patch_get_utility()
|
||||
def test_no_certname(self, mock_get_utility, mock_renewal_conf_files):
|
||||
mock_config = mock.Mock(certname=None, new_certname="two")
|
||||
|
||||
# if not choices
|
||||
mock_renewal_conf_files.return_value = []
|
||||
self.assertRaises(errors.Error, self._call, mock_config)
|
||||
|
||||
mock_renewal_conf_files.return_value = ["one.conf"]
|
||||
util_mock = mock.Mock()
|
||||
util_mock.menu.return_value = (display_util.CANCEL, 0)
|
||||
mock_get_utility.return_value = util_mock
|
||||
self.assertRaises(errors.Error, self._call, mock_config)
|
||||
|
||||
util_mock.menu.return_value = (display_util.OK, -1)
|
||||
self.assertRaises(errors.Error, self._call, mock_config)
|
||||
|
||||
@test_util.patch_get_utility()
|
||||
def test_no_new_certname(self, mock_get_utility):
|
||||
mock_config = mock.Mock(certname="one", new_certname=None)
|
||||
|
||||
util_mock = mock.Mock()
|
||||
util_mock.input.return_value = (display_util.CANCEL, "name")
|
||||
mock_get_utility.return_value = util_mock
|
||||
self.assertRaises(errors.Error, self._call, mock_config)
|
||||
|
||||
util_mock = mock.Mock()
|
||||
util_mock.input.return_value = (display_util.OK, None)
|
||||
mock_get_utility.return_value = util_mock
|
||||
self.assertRaises(errors.Error, self._call, mock_config)
|
||||
|
||||
@test_util.patch_get_utility()
|
||||
@mock.patch('certbot.cert_manager.lineage_for_certname')
|
||||
def test_no_existing_certname(self, mock_lineage_for_certname, unused_get_utility):
|
||||
mock_config = mock.Mock(certname="one", new_certname="two")
|
||||
mock_lineage_for_certname.return_value = None
|
||||
self.assertRaises(errors.ConfigurationError,
|
||||
self._call, mock_config)
|
||||
|
||||
@test_util.patch_get_utility()
|
||||
@mock.patch("certbot.storage.RenewableCert._check_symlinks")
|
||||
def test_rename_cert(self, mock_check, unused_get_utility):
|
||||
mock_check.return_value = True
|
||||
mock_config = self.mock_config
|
||||
self._call(mock_config)
|
||||
from certbot import cert_manager
|
||||
updated_lineage = cert_manager.lineage_for_certname(mock_config, mock_config.new_certname)
|
||||
self.assertTrue(updated_lineage is not None)
|
||||
self.assertEqual(updated_lineage.lineagename, mock_config.new_certname)
|
||||
|
||||
@test_util.patch_get_utility()
|
||||
@mock.patch("certbot.storage.RenewableCert._check_symlinks")
|
||||
def test_rename_cert_interactive_certname(self, mock_check, mock_get_utility):
|
||||
mock_check.return_value = True
|
||||
mock_config = self.mock_config
|
||||
mock_config.certname = None
|
||||
util_mock = mock.Mock()
|
||||
util_mock.menu.return_value = (display_util.OK, 0)
|
||||
mock_get_utility.return_value = util_mock
|
||||
self._call(mock_config)
|
||||
from certbot import cert_manager
|
||||
updated_lineage = cert_manager.lineage_for_certname(mock_config, mock_config.new_certname)
|
||||
self.assertTrue(updated_lineage is not None)
|
||||
self.assertEqual(updated_lineage.lineagename, mock_config.new_certname)
|
||||
|
||||
@test_util.patch_get_utility()
|
||||
@mock.patch("certbot.storage.RenewableCert._check_symlinks")
|
||||
def test_rename_cert_bad_new_certname(self, mock_check, unused_get_utility):
|
||||
mock_check.return_value = True
|
||||
mock_config = self.mock_config
|
||||
|
||||
# for example, don't rename to existing certname
|
||||
mock_config.new_certname = "example.org"
|
||||
self.assertRaises(errors.ConfigurationError, self._call, mock_config)
|
||||
|
||||
mock_config.new_certname = "one{0}two".format(os.path.sep)
|
||||
self.assertRaises(errors.ConfigurationError, self._call, mock_config)
|
||||
|
||||
|
||||
class DuplicativeCertsTest(storage_test.BaseRenewableCertTest):
|
||||
"""Test to avoid duplicate lineages."""
|
||||
|
||||
def setUp(self):
|
||||
super(DuplicativeCertsTest, self).setUp()
|
||||
self.config.write()
|
||||
self._write_out_ex_kinds()
|
||||
|
||||
def tearDown(self):
|
||||
shutil.rmtree(self.tempdir)
|
||||
|
||||
@mock.patch('certbot.util.make_or_verify_dir')
|
||||
def test_find_duplicative_names(self, unused_makedir):
|
||||
from certbot.cert_manager import find_duplicative_certs
|
||||
test_cert = test_util.load_vector('cert-san.pem')
|
||||
with open(self.test_rc.cert, 'wb') as f:
|
||||
f.write(test_cert)
|
||||
|
||||
# No overlap at all
|
||||
result = find_duplicative_certs(
|
||||
self.cli_config, ['wow.net', 'hooray.org'])
|
||||
self.assertEqual(result, (None, None))
|
||||
|
||||
# Totally identical
|
||||
result = find_duplicative_certs(
|
||||
self.cli_config, ['example.com', 'www.example.com'])
|
||||
self.assertTrue(result[0].configfile.filename.endswith('example.org.conf'))
|
||||
self.assertEqual(result[1], None)
|
||||
|
||||
# Superset
|
||||
result = find_duplicative_certs(
|
||||
self.cli_config, ['example.com', 'www.example.com', 'something.new'])
|
||||
self.assertEqual(result[0], None)
|
||||
self.assertTrue(result[1].configfile.filename.endswith('example.org.conf'))
|
||||
|
||||
# Partial overlap doesn't count
|
||||
result = find_duplicative_certs(
|
||||
self.cli_config, ['example.com', 'something.new'])
|
||||
self.assertEqual(result, (None, None))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main() # pragma: no cover
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -13,7 +13,7 @@ from certbot import account
|
|||
from certbot import errors
|
||||
from certbot import util
|
||||
|
||||
from certbot.tests import test_util
|
||||
import certbot.tests.util as test_util
|
||||
|
||||
|
||||
KEY = test_util.load_vector("rsa512_key.pem")
|
||||
|
|
@ -102,15 +102,18 @@ class RegisterTest(unittest.TestCase):
|
|||
mock_client().register.side_effect = [mx_err, mock.MagicMock()]
|
||||
self.assertRaises(messages.Error, self._call)
|
||||
|
||||
class ClientTest(unittest.TestCase):
|
||||
"""Tests for certbot.client.Client."""
|
||||
|
||||
class ClientTestCommon(unittest.TestCase):
|
||||
"""Common base class for certbot.client.Client tests."""
|
||||
def setUp(self):
|
||||
self.config = mock.MagicMock(
|
||||
no_verify_ssl=False, config_dir="/etc/letsencrypt", allow_subset_of_names=False)
|
||||
no_verify_ssl=False,
|
||||
config_dir="/etc/letsencrypt",
|
||||
work_dir="/var/lib/letsencrypt",
|
||||
allow_subset_of_names=False)
|
||||
|
||||
# pylint: disable=star-args
|
||||
self.account = mock.MagicMock(**{"key.pem": KEY})
|
||||
self.eg_domains = ["example.com", "www.example.com"]
|
||||
|
||||
from certbot.client import Client
|
||||
with mock.patch("certbot.client.acme_client.Client") as acme:
|
||||
|
|
@ -120,6 +123,16 @@ class ClientTest(unittest.TestCase):
|
|||
config=self.config, account_=self.account,
|
||||
auth=None, installer=None)
|
||||
|
||||
|
||||
class ClientTest(ClientTestCommon):
|
||||
"""Tests for certbot.client.Client."""
|
||||
def setUp(self):
|
||||
super(ClientTest, self).setUp()
|
||||
|
||||
self.config.allow_subset_of_names = False
|
||||
self.config.config_dir = "/etc/letsencrypt"
|
||||
self.eg_domains = ["example.com", "www.example.com"]
|
||||
|
||||
def test_init_acme_verify_ssl(self):
|
||||
net = self.acme_client.call_args[1]["net"]
|
||||
self.assertTrue(net.verify_ssl)
|
||||
|
|
@ -213,6 +226,27 @@ class ClientTest(unittest.TestCase):
|
|||
mock.sentinel.key, domains, self.config.csr_dir)
|
||||
self._check_obtain_certificate()
|
||||
|
||||
@mock.patch('certbot.client.Client.obtain_certificate')
|
||||
@mock.patch('certbot.storage.RenewableCert.new_lineage')
|
||||
@mock.patch('OpenSSL.crypto.dump_certificate')
|
||||
def test_obtain_and_enroll_certificate(self, mock_dump_certificate,
|
||||
mock_storage, mock_obtain_certificate):
|
||||
domains = ["example.com", "www.example.com"]
|
||||
mock_obtain_certificate.return_value = (mock.MagicMock(),
|
||||
mock.MagicMock(), mock.MagicMock(), None)
|
||||
|
||||
self.client.config.dry_run = False
|
||||
self.assertTrue(self.client.obtain_and_enroll_certificate(domains, "example_cert"))
|
||||
|
||||
self.assertTrue(self.client.obtain_and_enroll_certificate(domains, None))
|
||||
|
||||
self.client.config.dry_run = True
|
||||
|
||||
self.assertFalse(self.client.obtain_and_enroll_certificate(domains, None))
|
||||
|
||||
self.assertTrue(mock_storage.call_count == 2)
|
||||
self.assertTrue(mock_dump_certificate.call_count == 2)
|
||||
|
||||
@mock.patch("certbot.cli.helpful_parser")
|
||||
def test_save_certificate(self, mock_parser):
|
||||
# pylint: disable=too-many-locals
|
||||
|
|
@ -289,7 +323,7 @@ class ClientTest(unittest.TestCase):
|
|||
["foo.bar"], "key", "cert", "chain", "fullchain")
|
||||
installer.recovery_routine.assert_called_once_with()
|
||||
|
||||
@mock.patch("certbot.client.zope.component.getUtility")
|
||||
@test_util.patch_get_utility()
|
||||
def test_deploy_certificate_restart_failure(self, mock_get_utility):
|
||||
installer = mock.MagicMock()
|
||||
installer.restart.side_effect = [errors.PluginError, None]
|
||||
|
|
@ -301,7 +335,7 @@ class ClientTest(unittest.TestCase):
|
|||
installer.rollback_checkpoints.assert_called_once_with()
|
||||
self.assertEqual(installer.restart.call_count, 2)
|
||||
|
||||
@mock.patch("certbot.client.zope.component.getUtility")
|
||||
@test_util.patch_get_utility()
|
||||
def test_deploy_certificate_restart_failure2(self, mock_get_utility):
|
||||
installer = mock.MagicMock()
|
||||
installer.restart.side_effect = errors.PluginError
|
||||
|
|
@ -314,147 +348,109 @@ class ClientTest(unittest.TestCase):
|
|||
installer.rollback_checkpoints.assert_called_once_with()
|
||||
self.assertEqual(installer.restart.call_count, 1)
|
||||
|
||||
@mock.patch("certbot.client.enhancements")
|
||||
def test_enhance_config(self, mock_enhancements):
|
||||
config = ConfigHelper(redirect=True, hsts=False, uir=False)
|
||||
self.assertRaises(errors.Error, self.client.enhance_config,
|
||||
["foo.bar"], config, None)
|
||||
|
||||
mock_enhancements.ask.return_value = True
|
||||
installer = mock.MagicMock()
|
||||
self.client.installer = installer
|
||||
installer.supported_enhancements.return_value = ["redirect"]
|
||||
class EnhanceConfigTest(ClientTestCommon):
|
||||
"""Tests for certbot.client.Client.enhance_config."""
|
||||
def setUp(self):
|
||||
super(EnhanceConfigTest, self).setUp()
|
||||
|
||||
self.client.enhance_config(["foo.bar"], config, None)
|
||||
installer.enhance.assert_called_once_with("foo.bar", "redirect", None)
|
||||
self.assertEqual(installer.save.call_count, 1)
|
||||
installer.restart.assert_called_once_with()
|
||||
self.config.hsts = False
|
||||
self.config.redirect = False
|
||||
self.config.staple = False
|
||||
self.config.uir = False
|
||||
self.domain = "example.org"
|
||||
|
||||
def test_no_installer(self):
|
||||
self.assertRaises(
|
||||
errors.Error, self.client.enhance_config, [self.domain], None)
|
||||
|
||||
@mock.patch("certbot.client.enhancements")
|
||||
def test_enhance_config_no_ask(self, mock_enhancements):
|
||||
config = ConfigHelper(redirect=True, hsts=False,
|
||||
uir=False, staple=False)
|
||||
self.assertRaises(errors.Error, self.client.enhance_config,
|
||||
["foo.bar"], config, None)
|
||||
def test_unsupported(self, mock_enhancements):
|
||||
self.client.installer = mock.MagicMock()
|
||||
self.client.installer.supported_enhancements.return_value = []
|
||||
|
||||
mock_enhancements.ask.return_value = True
|
||||
installer = mock.MagicMock()
|
||||
self.client.installer = installer
|
||||
installer.supported_enhancements.return_value = [
|
||||
"redirect", "ensure-http-header", "staple-ocsp"]
|
||||
|
||||
config = ConfigHelper(redirect=True, hsts=False,
|
||||
uir=False, staple=False)
|
||||
self.client.enhance_config(["foo.bar"], config, None)
|
||||
installer.enhance.assert_called_with("foo.bar", "redirect", None)
|
||||
|
||||
config = ConfigHelper(redirect=False, hsts=True,
|
||||
uir=False, staple=False)
|
||||
self.client.enhance_config(["foo.bar"], config, None)
|
||||
installer.enhance.assert_called_with("foo.bar", "ensure-http-header",
|
||||
"Strict-Transport-Security")
|
||||
|
||||
config = ConfigHelper(redirect=False, hsts=False,
|
||||
uir=True, staple=False)
|
||||
self.client.enhance_config(["foo.bar"], config, None)
|
||||
installer.enhance.assert_called_with("foo.bar", "ensure-http-header",
|
||||
"Upgrade-Insecure-Requests")
|
||||
|
||||
config = ConfigHelper(redirect=False, hsts=False,
|
||||
uir=False, staple=True)
|
||||
self.client.enhance_config(["foo.bar"], config, None)
|
||||
installer.enhance.assert_called_with("foo.bar", "staple-ocsp", None)
|
||||
|
||||
self.assertEqual(installer.save.call_count, 4)
|
||||
self.assertEqual(installer.restart.call_count, 4)
|
||||
|
||||
@mock.patch("certbot.client.enhancements")
|
||||
def test_enhance_config_unsupported(self, mock_enhancements):
|
||||
installer = mock.MagicMock()
|
||||
self.client.installer = installer
|
||||
installer.supported_enhancements.return_value = []
|
||||
|
||||
config = ConfigHelper(redirect=None, hsts=True, uir=True)
|
||||
self.client.enhance_config(["foo.bar"], config, None)
|
||||
installer.enhance.assert_not_called()
|
||||
self.config.redirect = None
|
||||
self.config.hsts = True
|
||||
with mock.patch("certbot.client.logger") as mock_logger:
|
||||
self.client.enhance_config([self.domain], None)
|
||||
self.assertEqual(mock_logger.warning.call_count, 1)
|
||||
self.client.installer.enhance.assert_not_called()
|
||||
mock_enhancements.ask.assert_not_called()
|
||||
|
||||
def test_enhance_config_no_installer(self):
|
||||
config = ConfigHelper(redirect=True, hsts=False, uir=False)
|
||||
self.assertRaises(errors.Error, self.client.enhance_config,
|
||||
["foo.bar"], config, None)
|
||||
def test_no_ask_hsts(self):
|
||||
self.config.hsts = True
|
||||
self._test_with_all_supported()
|
||||
self.client.installer.enhance.assert_called_with(
|
||||
self.domain, "ensure-http-header", "Strict-Transport-Security")
|
||||
|
||||
@mock.patch("certbot.client.zope.component.getUtility")
|
||||
@mock.patch("certbot.client.enhancements")
|
||||
def test_enhance_config_enhance_failure(self, mock_enhancements,
|
||||
mock_get_utility):
|
||||
mock_enhancements.ask.return_value = True
|
||||
def test_no_ask_redirect(self):
|
||||
self.config.redirect = True
|
||||
self._test_with_all_supported()
|
||||
self.client.installer.enhance.assert_called_with(
|
||||
self.domain, "redirect", None)
|
||||
|
||||
def test_no_ask_staple(self):
|
||||
self.config.staple = True
|
||||
self._test_with_all_supported()
|
||||
self.client.installer.enhance.assert_called_with(
|
||||
self.domain, "staple-ocsp", None)
|
||||
|
||||
def test_no_ask_uir(self):
|
||||
self.config.uir = True
|
||||
self._test_with_all_supported()
|
||||
self.client.installer.enhance.assert_called_with(
|
||||
self.domain, "ensure-http-header", "Upgrade-Insecure-Requests")
|
||||
|
||||
def test_enhance_failure(self):
|
||||
self.client.installer = mock.MagicMock()
|
||||
self.client.installer.enhance.side_effect = errors.PluginError
|
||||
self._test_error()
|
||||
self.client.installer.recovery_routine.assert_called_once_with()
|
||||
|
||||
def test_save_failure(self):
|
||||
self.client.installer = mock.MagicMock()
|
||||
self.client.installer.save.side_effect = errors.PluginError
|
||||
self._test_error()
|
||||
self.client.installer.recovery_routine.assert_called_once_with()
|
||||
self.client.installer.save.assert_called_once_with(mock.ANY)
|
||||
|
||||
def test_restart_failure(self):
|
||||
self.client.installer = mock.MagicMock()
|
||||
self.client.installer.restart.side_effect = [errors.PluginError, None]
|
||||
self._test_error_with_rollback()
|
||||
|
||||
def test_restart_failure2(self):
|
||||
installer = mock.MagicMock()
|
||||
self.client.installer = installer
|
||||
installer.supported_enhancements.return_value = ["redirect"]
|
||||
installer.enhance.side_effect = errors.PluginError
|
||||
|
||||
config = ConfigHelper(redirect=True, hsts=False, uir=False)
|
||||
|
||||
self.assertRaises(errors.PluginError, self.client.enhance_config,
|
||||
["foo.bar"], config, None)
|
||||
installer.recovery_routine.assert_called_once_with()
|
||||
self.assertEqual(mock_get_utility().add_message.call_count, 1)
|
||||
|
||||
@mock.patch("certbot.client.zope.component.getUtility")
|
||||
@mock.patch("certbot.client.enhancements")
|
||||
def test_enhance_config_save_failure(self, mock_enhancements,
|
||||
mock_get_utility):
|
||||
mock_enhancements.ask.return_value = True
|
||||
installer = mock.MagicMock()
|
||||
self.client.installer = installer
|
||||
installer.supported_enhancements.return_value = ["redirect"]
|
||||
installer.save.side_effect = errors.PluginError
|
||||
|
||||
config = ConfigHelper(redirect=True, hsts=False, uir=False)
|
||||
|
||||
self.assertRaises(errors.PluginError, self.client.enhance_config,
|
||||
["foo.bar"], config, None)
|
||||
installer.recovery_routine.assert_called_once_with()
|
||||
self.assertEqual(mock_get_utility().add_message.call_count, 1)
|
||||
|
||||
@mock.patch("certbot.client.zope.component.getUtility")
|
||||
@mock.patch("certbot.client.enhancements")
|
||||
def test_enhance_config_restart_failure(self, mock_enhancements,
|
||||
mock_get_utility):
|
||||
mock_enhancements.ask.return_value = True
|
||||
installer = mock.MagicMock()
|
||||
self.client.installer = installer
|
||||
installer.supported_enhancements.return_value = ["redirect"]
|
||||
installer.restart.side_effect = [errors.PluginError, None]
|
||||
|
||||
config = ConfigHelper(redirect=True, hsts=False, uir=False)
|
||||
|
||||
self.assertRaises(errors.PluginError, self.client.enhance_config,
|
||||
["foo.bar"], config, None)
|
||||
|
||||
self.assertEqual(mock_get_utility().add_message.call_count, 1)
|
||||
installer.rollback_checkpoints.assert_called_once_with()
|
||||
self.assertEqual(installer.restart.call_count, 2)
|
||||
|
||||
@mock.patch("certbot.client.zope.component.getUtility")
|
||||
@mock.patch("certbot.client.enhancements")
|
||||
def test_enhance_config_restart_failure2(self, mock_enhancements,
|
||||
mock_get_utility):
|
||||
mock_enhancements.ask.return_value = True
|
||||
installer = mock.MagicMock()
|
||||
self.client.installer = installer
|
||||
installer.supported_enhancements.return_value = ["redirect"]
|
||||
installer.restart.side_effect = errors.PluginError
|
||||
installer.rollback_checkpoints.side_effect = errors.ReverterError
|
||||
self.client.installer = installer
|
||||
self._test_error_with_rollback()
|
||||
|
||||
config = ConfigHelper(redirect=True, hsts=False, uir=False)
|
||||
@mock.patch("certbot.client.enhancements.ask")
|
||||
def test_ask(self, mock_ask):
|
||||
self.config.redirect = None
|
||||
mock_ask.return_value = True
|
||||
self._test_with_all_supported()
|
||||
|
||||
self.assertRaises(errors.PluginError, self.client.enhance_config,
|
||||
["foo.bar"], config, None)
|
||||
self.assertEqual(mock_get_utility().add_message.call_count, 1)
|
||||
installer.rollback_checkpoints.assert_called_once_with()
|
||||
self.assertEqual(installer.restart.call_count, 1)
|
||||
def _test_error_with_rollback(self):
|
||||
self._test_error()
|
||||
self.assertTrue(self.client.installer.restart.called)
|
||||
|
||||
def _test_error(self):
|
||||
self.config.redirect = True
|
||||
with test_util.patch_get_utility() as mock_gu:
|
||||
self.assertRaises(
|
||||
errors.PluginError, self._test_with_all_supported)
|
||||
self.assertEqual(mock_gu().add_message.call_count, 1)
|
||||
|
||||
def _test_with_all_supported(self):
|
||||
if self.client.installer is None:
|
||||
self.client.installer = mock.MagicMock()
|
||||
self.client.installer.supported_enhancements.return_value = [
|
||||
"ensure-http-header", "redirect", "staple-ocsp"]
|
||||
self.client.enhance_config([self.domain], None)
|
||||
self.assertEqual(self.client.installer.save.call_count, 1)
|
||||
self.assertEqual(self.client.installer.restart.call_count, 1)
|
||||
|
||||
|
||||
class RollbackTest(unittest.TestCase):
|
||||
|
|
|
|||
|
|
@ -88,31 +88,19 @@ class NamespaceConfigTest(unittest.TestCase):
|
|||
self.assertTrue(os.path.isabs(config.key_dir))
|
||||
self.assertTrue(os.path.isabs(config.temp_checkpoint_dir))
|
||||
|
||||
|
||||
class RenewerConfigurationTest(unittest.TestCase):
|
||||
"""Test for certbot.configuration.RenewerConfiguration."""
|
||||
|
||||
def setUp(self):
|
||||
self.namespace = mock.MagicMock(config_dir='/tmp/config')
|
||||
from certbot.configuration import RenewerConfiguration
|
||||
self.config = RenewerConfiguration(self.namespace)
|
||||
|
||||
@mock.patch('certbot.configuration.constants')
|
||||
def test_dynamic_dirs(self, constants):
|
||||
def test_renewal_dynamic_dirs(self, constants):
|
||||
constants.ARCHIVE_DIR = 'a'
|
||||
constants.LIVE_DIR = 'l'
|
||||
constants.RENEWAL_CONFIGS_DIR = 'renewal_configs'
|
||||
constants.RENEWER_CONFIG_FILENAME = 'r.conf'
|
||||
|
||||
self.assertEqual(self.config.archive_dir, '/tmp/config/a')
|
||||
self.assertEqual(self.config.default_archive_dir, '/tmp/config/a')
|
||||
self.assertEqual(self.config.live_dir, '/tmp/config/l')
|
||||
self.assertEqual(
|
||||
self.config.renewal_configs_dir, '/tmp/config/renewal_configs')
|
||||
self.assertEqual(self.config.renewer_config_file, '/tmp/config/r.conf')
|
||||
|
||||
def test_absolute_paths(self):
|
||||
def test_renewal_absolute_paths(self):
|
||||
from certbot.configuration import NamespaceConfig
|
||||
from certbot.configuration import RenewerConfiguration
|
||||
|
||||
config_base = "foo"
|
||||
work_base = "bar"
|
||||
|
|
@ -125,12 +113,11 @@ class RenewerConfigurationTest(unittest.TestCase):
|
|||
mock_namespace.config_dir = config_base
|
||||
mock_namespace.work_dir = work_base
|
||||
mock_namespace.logs_dir = logs_base
|
||||
config = RenewerConfiguration(NamespaceConfig(mock_namespace))
|
||||
config = NamespaceConfig(mock_namespace)
|
||||
|
||||
self.assertTrue(os.path.isabs(config.archive_dir))
|
||||
self.assertTrue(os.path.isabs(config.default_archive_dir))
|
||||
self.assertTrue(os.path.isabs(config.live_dir))
|
||||
self.assertTrue(os.path.isabs(config.renewal_configs_dir))
|
||||
self.assertTrue(os.path.isabs(config.renewer_config_file))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import zope.component
|
|||
from certbot import errors
|
||||
from certbot import interfaces
|
||||
from certbot import util
|
||||
from certbot.tests import test_util
|
||||
import certbot.tests.util as test_util
|
||||
|
||||
|
||||
RSA256_KEY = test_util.load_vector('rsa256_key.pem')
|
||||
|
|
@ -40,9 +40,9 @@ class InitSaveKeyTest(unittest.TestCase):
|
|||
|
||||
@mock.patch('certbot.crypto_util.make_key')
|
||||
def test_success(self, mock_make):
|
||||
mock_make.return_value = 'key_pem'
|
||||
mock_make.return_value = b'key_pem'
|
||||
key = self._call(1024, self.key_dir)
|
||||
self.assertEqual(key.pem, 'key_pem')
|
||||
self.assertEqual(key.pem, b'key_pem')
|
||||
self.assertTrue('key-certbot.pem' in key.file)
|
||||
|
||||
@mock.patch('certbot.crypto_util.make_key')
|
||||
|
|
@ -67,13 +67,13 @@ class InitSaveCSRTest(unittest.TestCase):
|
|||
def test_it(self, unused_mock_verify, mock_csr):
|
||||
from certbot.crypto_util import init_save_csr
|
||||
|
||||
mock_csr.return_value = ('csr_pem', 'csr_der')
|
||||
mock_csr.return_value = (b'csr_pem', b'csr_der')
|
||||
|
||||
csr = init_save_csr(
|
||||
mock.Mock(pem='dummy_key'), 'example.com', self.csr_dir,
|
||||
'csr-certbot.pem')
|
||||
|
||||
self.assertEqual(csr.data, 'csr_der')
|
||||
self.assertEqual(csr.data, b'csr_der')
|
||||
self.assertTrue('csr-certbot.pem' in csr.file)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -13,11 +13,10 @@ from acme import messages
|
|||
|
||||
from certbot import account
|
||||
from certbot import errors
|
||||
from certbot import interfaces
|
||||
|
||||
from certbot.display import util as display_util
|
||||
|
||||
from certbot.tests import test_util
|
||||
import certbot.tests.util as test_util
|
||||
|
||||
|
||||
KEY = jose.JWKRSA.load(test_util.load_vector("rsa512_key.pem"))
|
||||
|
|
@ -26,57 +25,73 @@ KEY = jose.JWKRSA.load(test_util.load_vector("rsa512_key.pem"))
|
|||
class GetEmailTest(unittest.TestCase):
|
||||
"""Tests for certbot.display.ops.get_email."""
|
||||
|
||||
def setUp(self):
|
||||
mock_display = mock.MagicMock()
|
||||
self.input = mock_display.input
|
||||
zope.component.provideUtility(mock_display, interfaces.IDisplay)
|
||||
|
||||
@classmethod
|
||||
def _call(cls, **kwargs):
|
||||
from certbot.display.ops import get_email
|
||||
return get_email(**kwargs)
|
||||
|
||||
def test_cancel_none(self):
|
||||
self.input.return_value = (display_util.CANCEL, "foo@bar.baz")
|
||||
@test_util.patch_get_utility("certbot.display.ops.z_util")
|
||||
def test_cancel_none(self, mock_get_utility):
|
||||
mock_input = mock_get_utility().input
|
||||
mock_input.return_value = (display_util.CANCEL, "foo@bar.baz")
|
||||
self.assertRaises(errors.Error, self._call)
|
||||
self.assertRaises(errors.Error, self._call, optional=False)
|
||||
|
||||
def test_ok_safe(self):
|
||||
self.input.return_value = (display_util.OK, "foo@bar.baz")
|
||||
@test_util.patch_get_utility("certbot.display.ops.z_util")
|
||||
def test_ok_safe(self, mock_get_utility):
|
||||
mock_input = mock_get_utility().input
|
||||
mock_input.return_value = (display_util.OK, "foo@bar.baz")
|
||||
with mock.patch("certbot.display.ops.util.safe_email") as mock_safe_email:
|
||||
mock_safe_email.return_value = True
|
||||
self.assertTrue(self._call() is "foo@bar.baz")
|
||||
|
||||
def test_ok_not_safe(self):
|
||||
self.input.return_value = (display_util.OK, "foo@bar.baz")
|
||||
@test_util.patch_get_utility("certbot.display.ops.z_util")
|
||||
def test_ok_not_safe(self, mock_get_utility):
|
||||
mock_input = mock_get_utility().input
|
||||
mock_input.return_value = (display_util.OK, "foo@bar.baz")
|
||||
with mock.patch("certbot.display.ops.util.safe_email") as mock_safe_email:
|
||||
mock_safe_email.side_effect = [False, True]
|
||||
self.assertTrue(self._call() is "foo@bar.baz")
|
||||
|
||||
def test_invalid_flag(self):
|
||||
@test_util.patch_get_utility("certbot.display.ops.z_util")
|
||||
def test_invalid_flag(self, mock_get_utility):
|
||||
invalid_txt = "There seem to be problems"
|
||||
self.input.return_value = (display_util.OK, "foo@bar.baz")
|
||||
mock_input = mock_get_utility().input
|
||||
mock_input.return_value = (display_util.OK, "foo@bar.baz")
|
||||
with mock.patch("certbot.display.ops.util.safe_email") as mock_safe_email:
|
||||
mock_safe_email.return_value = True
|
||||
self._call()
|
||||
self.assertTrue(invalid_txt not in self.input.call_args[0][0])
|
||||
self.assertTrue(invalid_txt not in mock_input.call_args[0][0])
|
||||
self._call(invalid=True)
|
||||
self.assertTrue(invalid_txt in self.input.call_args[0][0])
|
||||
self.assertTrue(invalid_txt in mock_input.call_args[0][0])
|
||||
|
||||
def test_optional_flag(self):
|
||||
self.input.return_value = (display_util.OK, "foo@bar.baz")
|
||||
@test_util.patch_get_utility("certbot.display.ops.z_util")
|
||||
def test_optional_flag(self, mock_get_utility):
|
||||
mock_input = mock_get_utility().input
|
||||
mock_input.return_value = (display_util.OK, "foo@bar.baz")
|
||||
with mock.patch("certbot.display.ops.util.safe_email") as mock_safe_email:
|
||||
mock_safe_email.side_effect = [False, True]
|
||||
self._call(optional=False)
|
||||
for call in self.input.call_args_list:
|
||||
for call in mock_input.call_args_list:
|
||||
self.assertTrue(
|
||||
"--register-unsafely-without-email" not in call[0][0])
|
||||
|
||||
@test_util.patch_get_utility("certbot.display.ops.z_util")
|
||||
def test_optional_invalid_unsafe(self, mock_get_utility):
|
||||
invalid_txt = "There seem to be problems"
|
||||
mock_input = mock_get_utility().input
|
||||
mock_input.return_value = (display_util.OK, "foo@bar.baz")
|
||||
with mock.patch("certbot.display.ops.util.safe_email") as mock_safe_email:
|
||||
mock_safe_email.side_effect = [False, True]
|
||||
self._call(invalid=True)
|
||||
self.assertTrue(invalid_txt in mock_input.call_args[0][0])
|
||||
|
||||
|
||||
class ChooseAccountTest(unittest.TestCase):
|
||||
"""Tests for certbot.display.ops.choose_account."""
|
||||
def setUp(self):
|
||||
zope.component.provideUtility(display_util.FileDisplay(sys.stdout))
|
||||
zope.component.provideUtility(display_util.FileDisplay(sys.stdout,
|
||||
False))
|
||||
|
||||
self.accounts_dir = tempfile.mkdtemp("accounts")
|
||||
self.account_keys_dir = os.path.join(self.accounts_dir, "keys")
|
||||
|
|
@ -119,7 +134,8 @@ class ChooseAccountTest(unittest.TestCase):
|
|||
class GenSSLLabURLs(unittest.TestCase):
|
||||
"""Loose test of _gen_ssl_lab_urls. URL can change easily in the future."""
|
||||
def setUp(self):
|
||||
zope.component.provideUtility(display_util.FileDisplay(sys.stdout))
|
||||
zope.component.provideUtility(display_util.FileDisplay(sys.stdout,
|
||||
False))
|
||||
|
||||
@classmethod
|
||||
def _call(cls, domains):
|
||||
|
|
@ -138,7 +154,8 @@ class GenSSLLabURLs(unittest.TestCase):
|
|||
class GenHttpsNamesTest(unittest.TestCase):
|
||||
"""Test _gen_https_names."""
|
||||
def setUp(self):
|
||||
zope.component.provideUtility(display_util.FileDisplay(sys.stdout))
|
||||
zope.component.provideUtility(display_util.FileDisplay(sys.stdout,
|
||||
False))
|
||||
|
||||
@classmethod
|
||||
def _call(cls, domains):
|
||||
|
|
@ -185,7 +202,8 @@ class GenHttpsNamesTest(unittest.TestCase):
|
|||
class ChooseNamesTest(unittest.TestCase):
|
||||
"""Test choose names."""
|
||||
def setUp(self):
|
||||
zope.component.provideUtility(display_util.FileDisplay(sys.stdout))
|
||||
zope.component.provideUtility(display_util.FileDisplay(sys.stdout,
|
||||
False))
|
||||
self.mock_install = mock.MagicMock()
|
||||
|
||||
@classmethod
|
||||
|
|
@ -215,6 +233,45 @@ class ChooseNamesTest(unittest.TestCase):
|
|||
self.assertTrue(
|
||||
"configuration files" in mock_util().input.call_args[0][0])
|
||||
|
||||
def test_sort_names_trivial(self):
|
||||
from certbot.display.ops import _sort_names
|
||||
|
||||
#sort an empty list
|
||||
self.assertEqual(_sort_names([]), [])
|
||||
|
||||
#sort simple domains
|
||||
some_domains = ["ex.com", "zx.com", "ax.com"]
|
||||
self.assertEqual(_sort_names(some_domains), ["ax.com", "ex.com", "zx.com"])
|
||||
|
||||
#Sort subdomains of a single domain
|
||||
domain = ".ex.com"
|
||||
unsorted_short = ["e", "a", "z", "y"]
|
||||
unsorted_long = [us + domain for us in unsorted_short]
|
||||
|
||||
sorted_short = sorted(unsorted_short)
|
||||
sorted_long = [us + domain for us in sorted_short]
|
||||
|
||||
self.assertEqual(_sort_names(unsorted_long), sorted_long)
|
||||
|
||||
def test_sort_names_many(self):
|
||||
from certbot.display.ops import _sort_names
|
||||
|
||||
unsorted_domains = [".cx.com", ".bx.com", ".ax.com", ".dx.com"]
|
||||
unsorted_short = ["www", "bnother.long.subdomain", "a", "a.long.subdomain", "z", "b"]
|
||||
#Of course sorted doesn't work here ;-)
|
||||
sorted_short = ["a", "b", "a.long.subdomain", "bnother.long.subdomain", "www", "z"]
|
||||
|
||||
to_sort = []
|
||||
for short in unsorted_short:
|
||||
for domain in unsorted_domains:
|
||||
to_sort.append(short+domain)
|
||||
sortd = []
|
||||
for domain in sorted(unsorted_domains):
|
||||
for short in sorted_short:
|
||||
sortd.append(short+domain)
|
||||
self.assertEqual(_sort_names(to_sort), sortd)
|
||||
|
||||
|
||||
@mock.patch("certbot.display.ops.z_util")
|
||||
def test_filter_names_valid_return(self, mock_util):
|
||||
self.mock_install.get_all_names.return_value = set(["example.com"])
|
||||
|
|
@ -313,7 +370,7 @@ class SuccessRenewalTest(unittest.TestCase):
|
|||
@classmethod
|
||||
def _call(cls, names):
|
||||
from certbot.display.ops import success_renewal
|
||||
success_renewal(names, "renew")
|
||||
success_renewal(names)
|
||||
|
||||
@mock.patch("certbot.display.ops.z_util")
|
||||
def test_success_renewal(self, mock_util):
|
||||
|
|
@ -328,6 +385,21 @@ class SuccessRenewalTest(unittest.TestCase):
|
|||
for name in names:
|
||||
self.assertTrue(name in arg)
|
||||
|
||||
class SuccessRevocationTest(unittest.TestCase):
|
||||
# pylint: disable=too-few-public-methods
|
||||
"""Test the success revocation message."""
|
||||
@classmethod
|
||||
def _call(cls, path):
|
||||
from certbot.display.ops import success_revocation
|
||||
success_revocation(path)
|
||||
|
||||
@mock.patch("certbot.display.ops.z_util")
|
||||
def test_success_revocation(self, mock_util):
|
||||
mock_util().notification.return_value = None
|
||||
path = "/path/to/cert.pem"
|
||||
self._call(path)
|
||||
mock_util().notification.assert_called_once()
|
||||
self.assertTrue(path in mock_util().notification.call_args[0][0])
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main() # pragma: no cover
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
"""Test :mod:`certbot.display.util`."""
|
||||
import inspect
|
||||
import os
|
||||
import unittest
|
||||
|
||||
import mock
|
||||
|
||||
import certbot.errors as errors
|
||||
from certbot import errors
|
||||
from certbot import interfaces
|
||||
|
||||
from certbot.display import util as display_util
|
||||
|
||||
|
|
@ -13,122 +15,6 @@ CHOICES = [("First", "Description1"), ("Second", "Description2")]
|
|||
TAGS = ["tag1", "tag2", "tag3"]
|
||||
TAGS_CHOICES = [("1", "tag1"), ("2", "tag2"), ("3", "tag3")]
|
||||
|
||||
|
||||
class NcursesDisplayTest(unittest.TestCase):
|
||||
"""Test ncurses display.
|
||||
|
||||
Since this is mostly a wrapper, it might be more helpful to test the
|
||||
actual dialog boxes. The test file located in ./tests/display.py
|
||||
(relative to the root of the repository) will actually display the
|
||||
various boxes but requires the user to do the verification. If
|
||||
something seems amiss please use that test script to debug it, the
|
||||
automatic tests rely on too much mocking.
|
||||
|
||||
"""
|
||||
def setUp(self):
|
||||
super(NcursesDisplayTest, self).setUp()
|
||||
self.displayer = display_util.NcursesDisplay()
|
||||
|
||||
self.default_menu_options = {
|
||||
"choices": CHOICES,
|
||||
"ok_label": "OK",
|
||||
"cancel_label": "Cancel",
|
||||
"help_button": False,
|
||||
"help_label": "",
|
||||
"width": display_util.WIDTH,
|
||||
"height": display_util.HEIGHT,
|
||||
"menu_height": display_util.HEIGHT - 6,
|
||||
}
|
||||
|
||||
@mock.patch("certbot.display.util.dialog.Dialog.msgbox")
|
||||
def test_notification(self, mock_msgbox):
|
||||
"""Kind of worthless... one liner."""
|
||||
self.displayer.notification("message")
|
||||
self.assertEqual(mock_msgbox.call_count, 1)
|
||||
|
||||
@mock.patch("certbot.display.util.dialog.Dialog.menu")
|
||||
def test_menu_tag_and_desc(self, mock_menu):
|
||||
mock_menu.return_value = (display_util.OK, "First")
|
||||
|
||||
ret = self.displayer.menu("Message", CHOICES)
|
||||
mock_menu.assert_called_with("Message", **self.default_menu_options)
|
||||
|
||||
self.assertEqual(ret, (display_util.OK, 0))
|
||||
|
||||
@mock.patch("certbot.display.util.dialog.Dialog.menu")
|
||||
def test_menu_tag_and_desc_cancel(self, mock_menu):
|
||||
mock_menu.return_value = (display_util.CANCEL, "")
|
||||
|
||||
ret = self.displayer.menu("Message", CHOICES)
|
||||
|
||||
mock_menu.assert_called_with("Message", **self.default_menu_options)
|
||||
|
||||
self.assertEqual(ret, (display_util.CANCEL, -1))
|
||||
|
||||
@mock.patch("certbot.display.util.dialog.Dialog.menu")
|
||||
def test_menu_desc_only(self, mock_menu):
|
||||
mock_menu.return_value = (display_util.OK, "1")
|
||||
|
||||
ret = self.displayer.menu("Message", TAGS, help_label="More Info")
|
||||
|
||||
self.default_menu_options.update(
|
||||
choices=TAGS_CHOICES, help_button=True, help_label="More Info")
|
||||
mock_menu.assert_called_with("Message", **self.default_menu_options)
|
||||
|
||||
self.assertEqual(ret, (display_util.OK, 0))
|
||||
|
||||
@mock.patch("certbot.display.util.dialog.Dialog.menu")
|
||||
def test_menu_desc_only_help(self, mock_menu):
|
||||
mock_menu.return_value = (display_util.HELP, "2")
|
||||
|
||||
ret = self.displayer.menu("Message", TAGS, help_label="More Info")
|
||||
|
||||
self.assertEqual(ret, (display_util.HELP, 1))
|
||||
|
||||
@mock.patch("certbot.display.util.dialog.Dialog.menu")
|
||||
def test_menu_desc_only_cancel(self, mock_menu):
|
||||
mock_menu.return_value = (display_util.CANCEL, "")
|
||||
|
||||
ret = self.displayer.menu("Message", TAGS, help_label="More Info")
|
||||
|
||||
self.assertEqual(ret, (display_util.CANCEL, -1))
|
||||
|
||||
@mock.patch("certbot.display.util."
|
||||
"dialog.Dialog.inputbox")
|
||||
def test_input(self, mock_input):
|
||||
mock_input.return_value = (mock.MagicMock(), mock.MagicMock())
|
||||
self.displayer.input("message")
|
||||
self.assertEqual(mock_input.call_count, 1)
|
||||
|
||||
@mock.patch("certbot.display.util.dialog.Dialog.yesno")
|
||||
def test_yesno(self, mock_yesno):
|
||||
mock_yesno.return_value = display_util.OK
|
||||
|
||||
self.assertTrue(self.displayer.yesno("message"))
|
||||
|
||||
mock_yesno.assert_called_with(
|
||||
"message", yes_label="Yes", no_label="No")
|
||||
|
||||
@mock.patch("certbot.display.util."
|
||||
"dialog.Dialog.checklist")
|
||||
def test_checklist(self, mock_checklist):
|
||||
mock_checklist.return_value = (mock.MagicMock(), mock.MagicMock())
|
||||
self.displayer.checklist("message", TAGS)
|
||||
|
||||
choices = [
|
||||
(TAGS[0], "", True),
|
||||
(TAGS[1], "", True),
|
||||
(TAGS[2], "", True),
|
||||
]
|
||||
mock_checklist.assert_called_with("message", choices=choices)
|
||||
|
||||
@mock.patch("certbot.display.util.dialog.Dialog.dselect")
|
||||
def test_directory_select(self, mock_dselect):
|
||||
mock_dselect.return_value = (mock.MagicMock(), mock.MagicMock())
|
||||
self.displayer.directory_select("message")
|
||||
self.assertEqual(mock_dselect.call_count, 1)
|
||||
|
||||
|
||||
class FileOutputDisplayTest(unittest.TestCase):
|
||||
"""Test stdout display.
|
||||
|
||||
|
|
@ -136,85 +22,149 @@ class FileOutputDisplayTest(unittest.TestCase):
|
|||
functions look to a user, uncomment the test_visual function.
|
||||
|
||||
"""
|
||||
# pylint:disable=too-many-public-methods
|
||||
def setUp(self):
|
||||
super(FileOutputDisplayTest, self).setUp()
|
||||
self.mock_stdout = mock.MagicMock()
|
||||
self.displayer = display_util.FileDisplay(self.mock_stdout)
|
||||
self.displayer = display_util.FileDisplay(self.mock_stdout, False)
|
||||
|
||||
def test_notification_no_pause(self):
|
||||
self.displayer.notification("message", 10, False)
|
||||
self.displayer.notification("message", False)
|
||||
string = self.mock_stdout.write.call_args[0][0]
|
||||
|
||||
self.assertTrue("message" in string)
|
||||
|
||||
def test_notification_pause(self):
|
||||
with mock.patch("six.moves.input", return_value="enter"):
|
||||
self.displayer.notification("message")
|
||||
self.displayer.notification("message", force_interactive=True)
|
||||
|
||||
self.assertTrue("message" in self.mock_stdout.write.call_args[0][0])
|
||||
|
||||
def test_notification_noninteractive(self):
|
||||
self._force_noninteractive(self.displayer.notification, "message")
|
||||
string = self.mock_stdout.write.call_args[0][0]
|
||||
self.assertTrue("message" in string)
|
||||
|
||||
def test_notification_noninteractive2(self):
|
||||
# The main purpose of this test is to make sure we only call
|
||||
# logger.warning once which _force_noninteractive checks internally
|
||||
self._force_noninteractive(self.displayer.notification, "message")
|
||||
string = self.mock_stdout.write.call_args[0][0]
|
||||
self.assertTrue("message" in string)
|
||||
|
||||
self.assertTrue(self.displayer.skipped_interaction)
|
||||
|
||||
self._force_noninteractive(self.displayer.notification, "message2")
|
||||
string = self.mock_stdout.write.call_args[0][0]
|
||||
self.assertTrue("message2" in string)
|
||||
|
||||
@mock.patch("certbot.display.util."
|
||||
"FileDisplay._get_valid_int_ans")
|
||||
def test_menu(self, mock_ans):
|
||||
mock_ans.return_value = (display_util.OK, 1)
|
||||
ret = self.displayer.menu("message", CHOICES)
|
||||
ret = self.displayer.menu("message", CHOICES, force_interactive=True)
|
||||
self.assertEqual(ret, (display_util.OK, 0))
|
||||
|
||||
def test_menu_noninteractive(self):
|
||||
default = 0
|
||||
result = self._force_noninteractive(
|
||||
self.displayer.menu, "msg", CHOICES, default=default)
|
||||
self.assertEqual(result, (display_util.OK, default))
|
||||
|
||||
def test_input_cancel(self):
|
||||
with mock.patch("six.moves.input", return_value="c"):
|
||||
code, _ = self.displayer.input("message")
|
||||
code, _ = self.displayer.input("message", force_interactive=True)
|
||||
|
||||
self.assertTrue(code, display_util.CANCEL)
|
||||
|
||||
def test_input_normal(self):
|
||||
with mock.patch("six.moves.input", return_value="domain.com"):
|
||||
code, input_ = self.displayer.input("message")
|
||||
code, input_ = self.displayer.input("message", force_interactive=True)
|
||||
|
||||
self.assertEqual(code, display_util.OK)
|
||||
self.assertEqual(input_, "domain.com")
|
||||
|
||||
def test_input_noninteractive(self):
|
||||
default = "foo"
|
||||
code, input_ = self._force_noninteractive(
|
||||
self.displayer.input, "message", default=default)
|
||||
|
||||
self.assertEqual(code, display_util.OK)
|
||||
self.assertEqual(input_, default)
|
||||
|
||||
def test_input_assertion_fail(self):
|
||||
# If the call to util.assert_valid_call is commented out, an
|
||||
# error.Error is raised, otherwise, an AssertionError is raised.
|
||||
self.assertRaises(Exception, self._force_noninteractive,
|
||||
self.displayer.input, "message", cli_flag="--flag")
|
||||
|
||||
def test_input_assertion_fail2(self):
|
||||
with mock.patch("certbot.display.util.assert_valid_call"):
|
||||
self.assertRaises(errors.Error, self._force_noninteractive,
|
||||
self.displayer.input, "msg", cli_flag="--flag")
|
||||
|
||||
def test_yesno(self):
|
||||
with mock.patch("six.moves.input", return_value="Yes"):
|
||||
self.assertTrue(self.displayer.yesno("message"))
|
||||
self.assertTrue(self.displayer.yesno(
|
||||
"message", force_interactive=True))
|
||||
with mock.patch("six.moves.input", return_value="y"):
|
||||
self.assertTrue(self.displayer.yesno("message"))
|
||||
self.assertTrue(self.displayer.yesno(
|
||||
"message", force_interactive=True))
|
||||
with mock.patch("six.moves.input", side_effect=["maybe", "y"]):
|
||||
self.assertTrue(self.displayer.yesno("message"))
|
||||
self.assertTrue(self.displayer.yesno(
|
||||
"message", force_interactive=True))
|
||||
with mock.patch("six.moves.input", return_value="No"):
|
||||
self.assertFalse(self.displayer.yesno("message"))
|
||||
self.assertFalse(self.displayer.yesno(
|
||||
"message", force_interactive=True))
|
||||
with mock.patch("six.moves.input", side_effect=["cancel", "n"]):
|
||||
self.assertFalse(self.displayer.yesno("message"))
|
||||
self.assertFalse(self.displayer.yesno(
|
||||
"message", force_interactive=True))
|
||||
|
||||
with mock.patch("six.moves.input", return_value="a"):
|
||||
self.assertTrue(self.displayer.yesno("msg", yes_label="Agree"))
|
||||
self.assertTrue(self.displayer.yesno(
|
||||
"msg", yes_label="Agree", force_interactive=True))
|
||||
|
||||
@mock.patch("certbot.display.util.FileDisplay.input")
|
||||
def test_yesno_noninteractive(self):
|
||||
self.assertTrue(self._force_noninteractive(
|
||||
self.displayer.yesno, "message", default=True))
|
||||
|
||||
@mock.patch("certbot.display.util.six.moves.input")
|
||||
def test_checklist_valid(self, mock_input):
|
||||
mock_input.return_value = (display_util.OK, "2 1")
|
||||
code, tag_list = self.displayer.checklist("msg", TAGS)
|
||||
mock_input.return_value = "2 1"
|
||||
code, tag_list = self.displayer.checklist(
|
||||
"msg", TAGS, force_interactive=True)
|
||||
self.assertEqual(
|
||||
(code, set(tag_list)), (display_util.OK, set(["tag1", "tag2"])))
|
||||
|
||||
@mock.patch("certbot.display.util.FileDisplay.input")
|
||||
def test_checklist_miss_valid(self, mock_input):
|
||||
mock_input.side_effect = [
|
||||
(display_util.OK, "10"),
|
||||
(display_util.OK, "tag1 please"),
|
||||
(display_util.OK, "1")
|
||||
]
|
||||
@mock.patch("certbot.display.util.six.moves.input")
|
||||
def test_checklist_empty(self, mock_input):
|
||||
mock_input.return_value = ""
|
||||
code, tag_list = self.displayer.checklist("msg", TAGS, force_interactive=True)
|
||||
self.assertEqual(
|
||||
(code, set(tag_list)), (display_util.OK, set(["tag1", "tag2", "tag3"])))
|
||||
|
||||
ret = self.displayer.checklist("msg", TAGS)
|
||||
@mock.patch("certbot.display.util.six.moves.input")
|
||||
def test_checklist_miss_valid(self, mock_input):
|
||||
mock_input.side_effect = ["10", "tag1 please", "1"]
|
||||
|
||||
ret = self.displayer.checklist("msg", TAGS, force_interactive=True)
|
||||
self.assertEqual(ret, (display_util.OK, ["tag1"]))
|
||||
|
||||
@mock.patch("certbot.display.util.FileDisplay.input")
|
||||
@mock.patch("certbot.display.util.six.moves.input")
|
||||
def test_checklist_miss_quit(self, mock_input):
|
||||
mock_input.side_effect = [
|
||||
(display_util.OK, "10"),
|
||||
(display_util.CANCEL, "1")
|
||||
]
|
||||
ret = self.displayer.checklist("msg", TAGS)
|
||||
mock_input.side_effect = ["10", "c"]
|
||||
|
||||
ret = self.displayer.checklist("msg", TAGS, force_interactive=True)
|
||||
self.assertEqual(ret, (display_util.CANCEL, []))
|
||||
|
||||
def test_checklist_noninteractive(self):
|
||||
default = TAGS
|
||||
code, input_ = self._force_noninteractive(
|
||||
self.displayer.checklist, "msg", TAGS, default=default)
|
||||
|
||||
self.assertEqual(code, display_util.OK)
|
||||
self.assertEqual(input_, default)
|
||||
|
||||
def test_scrub_checklist_input_valid(self):
|
||||
# pylint: disable=protected-access
|
||||
indices = [
|
||||
|
|
@ -232,14 +182,38 @@ class FileOutputDisplayTest(unittest.TestCase):
|
|||
self.displayer._scrub_checklist_input(list_, TAGS))
|
||||
self.assertEqual(set_tags, exp[i])
|
||||
|
||||
@mock.patch("certbot.display.util.FileDisplay.input")
|
||||
@mock.patch("certbot.display.util.six.moves.input")
|
||||
def test_directory_select(self, mock_input):
|
||||
message = "msg"
|
||||
result = (display_util.OK, "/var/www/html",)
|
||||
mock_input.return_value = result
|
||||
# pylint: disable=star-args
|
||||
args = ["msg", "/var/www/html", "--flag", True]
|
||||
user_input = "/var/www/html"
|
||||
mock_input.return_value = user_input
|
||||
|
||||
self.assertEqual(self.displayer.directory_select(message), result)
|
||||
mock_input.assert_called_once_with(message)
|
||||
returned = self.displayer.directory_select(*args)
|
||||
self.assertEqual(returned, (display_util.OK, user_input))
|
||||
|
||||
def test_directory_select_noninteractive(self):
|
||||
default = "/var/www/html"
|
||||
code, input_ = self._force_noninteractive(
|
||||
self.displayer.directory_select, "msg", default=default)
|
||||
|
||||
self.assertEqual(code, display_util.OK)
|
||||
self.assertEqual(input_, default)
|
||||
|
||||
def _force_noninteractive(self, func, *args, **kwargs):
|
||||
skipped_interaction = self.displayer.skipped_interaction
|
||||
|
||||
with mock.patch("certbot.display.util.sys.stdin") as mock_stdin:
|
||||
mock_stdin.isatty.return_value = False
|
||||
with mock.patch("certbot.display.util.logger") as mock_logger:
|
||||
result = func(*args, **kwargs)
|
||||
|
||||
if skipped_interaction:
|
||||
self.assertFalse(mock_logger.warning.called)
|
||||
else:
|
||||
self.assertEqual(mock_logger.warning.call_count, 1)
|
||||
|
||||
return result
|
||||
|
||||
def test_scrub_checklist_input_invalid(self):
|
||||
# pylint: disable=protected-access
|
||||
|
|
@ -294,6 +268,13 @@ class FileOutputDisplayTest(unittest.TestCase):
|
|||
self.displayer._get_valid_int_ans(3),
|
||||
(display_util.CANCEL, -1))
|
||||
|
||||
def test_methods_take_force_interactive(self):
|
||||
# Every IDisplay method implemented by FileDisplay must take
|
||||
# force_interactive to prevent workflow regressions.
|
||||
for name in interfaces.IDisplay.names(): # pylint: disable=no-member
|
||||
arg_spec = inspect.getargspec(getattr(self.displayer, name))
|
||||
self.assertTrue("force_interactive" in arg_spec.args)
|
||||
|
||||
|
||||
class NoninteractiveDisplayTest(unittest.TestCase):
|
||||
"""Test non-interactive display.
|
||||
|
|
@ -344,6 +325,16 @@ class NoninteractiveDisplayTest(unittest.TestCase):
|
|||
self.assertRaises(
|
||||
errors.MissingCommandlineFlag, self.displayer.directory_select, "msg")
|
||||
|
||||
def test_methods_take_kwargs(self):
|
||||
# Every IDisplay method implemented by NoninteractiveDisplay
|
||||
# should take **kwargs because every method of FileDisplay must
|
||||
# take force_interactive which doesn't apply to
|
||||
# NoninteractiveDisplay.
|
||||
for name in interfaces.IDisplay.names(): # pylint: disable=no-member
|
||||
method = getattr(self.displayer, name)
|
||||
# asserts method accepts arbitrary keyword arguments
|
||||
self.assertFalse(inspect.getargspec(method).keywords is None)
|
||||
|
||||
|
||||
class SeparateListInputTest(unittest.TestCase):
|
||||
"""Test Module functions."""
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import unittest
|
|||
|
||||
import mock
|
||||
|
||||
|
||||
def get_signals(signums):
|
||||
"""Get the handlers for an iterable of signums."""
|
||||
return dict((s, signal.getsignal(s)) for s in signums)
|
||||
|
|
|
|||
|
|
@ -27,53 +27,57 @@ class HookTest(unittest.TestCase):
|
|||
config = mock.MagicMock(pre_hook="explodinator", post_hook="", renew_hook="")
|
||||
self.assertRaises(errors.HookCommandNotFound, hooks.validate_hooks, config)
|
||||
|
||||
@mock.patch('certbot.hooks._is_exe')
|
||||
def test_which(self, mock_is_exe):
|
||||
mock_is_exe.return_value = True
|
||||
self.assertEqual(hooks._which("/path/to/something"), "/path/to/something")
|
||||
|
||||
with mock.patch.dict('os.environ', {"PATH": "/floop:/fleep"}):
|
||||
mock_is_exe.return_value = True
|
||||
self.assertEqual(hooks._which("pingify"), "/floop/pingify")
|
||||
mock_is_exe.return_value = False
|
||||
self.assertEqual(hooks._which("pingify"), None)
|
||||
self.assertEqual(hooks._which("/path/to/something"), None)
|
||||
|
||||
@mock.patch('certbot.hooks._which')
|
||||
def test_prog(self, mockwhich):
|
||||
mockwhich.return_value = "/very/very/funky"
|
||||
@mock.patch('certbot.hooks.util.exe_exists')
|
||||
@mock.patch('certbot.hooks.plug_util.path_surgery')
|
||||
def test_prog(self, mock_ps, mock_exe_exists):
|
||||
mock_exe_exists.return_value = True
|
||||
self.assertEqual(hooks._prog("funky"), "funky")
|
||||
mockwhich.return_value = None
|
||||
self.assertEqual(mock_ps.call_count, 0)
|
||||
mock_exe_exists.return_value = False
|
||||
self.assertEqual(hooks._prog("funky"), None)
|
||||
self.assertEqual(mock_ps.call_count, 1)
|
||||
|
||||
def _test_a_hook(self, config, hook_function, calls_expected):
|
||||
def _test_a_hook(self, config, hook_function, calls_expected, **kwargs):
|
||||
with mock.patch('certbot.hooks.logger') as mock_logger:
|
||||
mock_logger.warning = mock.MagicMock()
|
||||
with mock.patch('certbot.hooks._run_hook') as mock_run_hook:
|
||||
hook_function(config)
|
||||
hook_function(config)
|
||||
hook_function(config, **kwargs)
|
||||
hook_function(config, **kwargs)
|
||||
self.assertEqual(mock_run_hook.call_count, calls_expected)
|
||||
return mock_logger.warning
|
||||
|
||||
def test_pre_hook(self):
|
||||
hooks.pre_hook.already = False
|
||||
hooks.pre_hook.already = set()
|
||||
config = mock.MagicMock(pre_hook="true")
|
||||
self._test_a_hook(config, hooks.pre_hook, 1)
|
||||
self._test_a_hook(config, hooks.pre_hook, 0)
|
||||
config = mock.MagicMock(pre_hook="more_true")
|
||||
self._test_a_hook(config, hooks.pre_hook, 1)
|
||||
self._test_a_hook(config, hooks.pre_hook, 0)
|
||||
config = mock.MagicMock(pre_hook="")
|
||||
self._test_a_hook(config, hooks.pre_hook, 0)
|
||||
|
||||
def test_post_hook(self):
|
||||
hooks.pre_hook.already = False
|
||||
# if pre-hook isn't called, post-hook shouldn't be
|
||||
config = mock.MagicMock(post_hook="true", verb="splonk")
|
||||
self._test_a_hook(config, hooks.post_hook, 0)
|
||||
def _test_renew_post_hooks(self, expected_count):
|
||||
with mock.patch('certbot.hooks.logger.info') as mock_info:
|
||||
with mock.patch('certbot.hooks._run_hook') as mock_run:
|
||||
hooks.run_saved_post_hooks()
|
||||
self.assertEqual(mock_run.call_count, expected_count)
|
||||
self.assertEqual(mock_info.call_count, expected_count)
|
||||
|
||||
def test_post_hooks(self):
|
||||
config = mock.MagicMock(post_hook="true", verb="splonk")
|
||||
self._test_a_hook(config, hooks.pre_hook, 1)
|
||||
self._test_a_hook(config, hooks.post_hook, 2)
|
||||
self._test_renew_post_hooks(0)
|
||||
|
||||
config = mock.MagicMock(post_hook="true", verb="renew")
|
||||
self._test_a_hook(config, hooks.post_hook, 0)
|
||||
self._test_renew_post_hooks(1)
|
||||
self._test_a_hook(config, hooks.post_hook, 0)
|
||||
self._test_renew_post_hooks(1)
|
||||
|
||||
config = mock.MagicMock(post_hook="more_true", verb="renew")
|
||||
self._test_a_hook(config, hooks.post_hook, 0)
|
||||
self._test_renew_post_hooks(2)
|
||||
|
||||
def test_renew_hook(self):
|
||||
with mock.patch.dict('os.environ', {}):
|
||||
|
|
|
|||
|
|
@ -1,48 +0,0 @@
|
|||
"""Tests for certbot.log."""
|
||||
import logging
|
||||
import unittest
|
||||
|
||||
import mock
|
||||
|
||||
|
||||
class DialogHandlerTest(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.d = mock.MagicMock()
|
||||
|
||||
from certbot.log import DialogHandler
|
||||
self.handler = DialogHandler(height=2, width=6, d=self.d)
|
||||
self.handler.PADDING_HEIGHT = 2
|
||||
self.handler.PADDING_WIDTH = 4
|
||||
|
||||
def test_adds_padding(self):
|
||||
self.handler.emit(logging.makeLogRecord({}))
|
||||
self.d.infobox.assert_called_once_with(mock.ANY, 4, 10)
|
||||
|
||||
def test_args_in_msg_get_replaced(self):
|
||||
assert len('123456') <= self.handler.width
|
||||
self.handler.emit(logging.makeLogRecord(
|
||||
{'msg': '123%s', 'args': (456,)}))
|
||||
self.d.infobox.assert_called_once_with('123456', mock.ANY, mock.ANY)
|
||||
|
||||
def test_wraps_nospace_is_greedy(self):
|
||||
assert len('1234567') > self.handler.width
|
||||
self.handler.emit(logging.makeLogRecord({'msg': '1234567'}))
|
||||
self.d.infobox.assert_called_once_with('123456\n7', mock.ANY, mock.ANY)
|
||||
|
||||
def test_wraps_at_whitespace(self):
|
||||
assert len('123 567') > self.handler.width
|
||||
self.handler.emit(logging.makeLogRecord({'msg': '123 567'}))
|
||||
self.d.infobox.assert_called_once_with('123\n567', mock.ANY, mock.ANY)
|
||||
|
||||
def test_only_last_lines_are_printed(self):
|
||||
assert len('a\nb\nc'.split()) > self.handler.height
|
||||
self.handler.emit(logging.makeLogRecord({'msg': 'a\n\nb\nc'}))
|
||||
self.d.infobox.assert_called_once_with('b\nc', mock.ANY, mock.ANY)
|
||||
|
||||
def test_non_str(self):
|
||||
self.handler.emit(logging.makeLogRecord({'msg': {'foo': 'bar'}}))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main() # pragma: no cover
|
||||
File diff suppressed because it is too large
Load diff
170
certbot/tests/ocsp_test.py
Normal file
170
certbot/tests/ocsp_test.py
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
"""Tests for ocsp.py"""
|
||||
# pylint: disable=protected-access
|
||||
|
||||
import unittest
|
||||
|
||||
import mock
|
||||
|
||||
from certbot import errors
|
||||
|
||||
out = """Missing = in header key=value
|
||||
ocsp: Use -help for summary.
|
||||
"""
|
||||
|
||||
class OCSPTest(unittest.TestCase):
|
||||
|
||||
_multiprocess_can_split_ = True
|
||||
|
||||
def setUp(self):
|
||||
from certbot import ocsp
|
||||
with mock.patch('certbot.ocsp.Popen') as mock_popen:
|
||||
with mock.patch('certbot.util.exe_exists') as mock_exists:
|
||||
mock_communicate = mock.MagicMock()
|
||||
mock_communicate.communicate.return_value = (None, out)
|
||||
mock_popen.return_value = mock_communicate
|
||||
mock_exists.return_value = True
|
||||
self.checker = ocsp.RevocationChecker()
|
||||
|
||||
def tearDown(self):
|
||||
pass
|
||||
|
||||
@mock.patch('certbot.ocsp.logging.info')
|
||||
@mock.patch('certbot.ocsp.Popen')
|
||||
@mock.patch('certbot.util.exe_exists')
|
||||
def test_init(self, mock_exists, mock_popen, mock_log):
|
||||
mock_communicate = mock.MagicMock()
|
||||
mock_communicate.communicate.return_value = (None, out)
|
||||
mock_popen.return_value = mock_communicate
|
||||
mock_exists.return_value = True
|
||||
|
||||
from certbot import ocsp
|
||||
checker = ocsp.RevocationChecker()
|
||||
self.assertEqual(mock_popen.call_count, 1)
|
||||
self.assertEqual(checker.host_args("x"), ["Host=x"])
|
||||
|
||||
mock_communicate.communicate.return_value = (None, out.partition("\n")[2])
|
||||
checker = ocsp.RevocationChecker()
|
||||
self.assertEqual(checker.host_args("x"), ["Host", "x"])
|
||||
self.assertEqual(checker.broken, False)
|
||||
|
||||
mock_exists.return_value = False
|
||||
mock_popen.call_count = 0
|
||||
checker = ocsp.RevocationChecker()
|
||||
self.assertEqual(mock_popen.call_count, 0)
|
||||
self.assertEqual(mock_log.call_count, 1)
|
||||
self.assertEqual(checker.broken, True)
|
||||
|
||||
@mock.patch('certbot.ocsp.RevocationChecker.determine_ocsp_server')
|
||||
@mock.patch('certbot.util.run_script')
|
||||
def test_ocsp_revoked(self, mock_run, mock_determine):
|
||||
self.checker.broken = True
|
||||
mock_determine.return_value = ("", "")
|
||||
self.assertEqual(self.checker.ocsp_revoked("x", "y"), False)
|
||||
|
||||
self.checker.broken = False
|
||||
mock_run.return_value = tuple(openssl_happy[1:])
|
||||
self.assertEqual(self.checker.ocsp_revoked("x", "y"), False)
|
||||
self.assertEqual(mock_run.call_count, 0)
|
||||
|
||||
mock_determine.return_value = ("http://x.co", "x.co")
|
||||
self.assertEqual(self.checker.ocsp_revoked("blah.pem", "chain.pem"), False)
|
||||
mock_run.side_effect = errors.SubprocessError("Unable to load certificate launcher")
|
||||
self.assertEqual(self.checker.ocsp_revoked("x", "y"), False)
|
||||
self.assertEqual(mock_run.call_count, 2)
|
||||
|
||||
|
||||
@mock.patch('certbot.ocsp.logger.info')
|
||||
@mock.patch('certbot.util.run_script')
|
||||
def test_determine_ocsp_server(self, mock_run, mock_info):
|
||||
uri = "http://ocsp.stg-int-x1.letsencrypt.org/"
|
||||
host = "ocsp.stg-int-x1.letsencrypt.org"
|
||||
mock_run.return_value = uri, ""
|
||||
self.assertEqual(self.checker.determine_ocsp_server("beep"), (uri, host))
|
||||
mock_run.return_value = "ftp:/" + host + "/", ""
|
||||
self.assertEqual(self.checker.determine_ocsp_server("beep"), (None, None))
|
||||
self.assertEqual(mock_info.call_count, 1)
|
||||
|
||||
c = "confusion"
|
||||
mock_run.side_effect = errors.SubprocessError(c)
|
||||
self.assertEqual(self.checker.determine_ocsp_server("beep"), (None, None))
|
||||
|
||||
@mock.patch('certbot.ocsp.logger')
|
||||
@mock.patch('certbot.util.run_script')
|
||||
def test_translate_ocsp(self, mock_run, mock_log):
|
||||
# pylint: disable=protected-access,star-args
|
||||
mock_run.return_value = openssl_confused
|
||||
from certbot import ocsp
|
||||
self.assertEqual(ocsp._translate_ocsp_query(*openssl_happy), False)
|
||||
self.assertEqual(ocsp._translate_ocsp_query(*openssl_confused), False)
|
||||
self.assertEqual(mock_log.debug.call_count, 1)
|
||||
self.assertEqual(mock_log.warn.call_count, 0)
|
||||
mock_log.debug.call_count = 0
|
||||
self.assertEqual(ocsp._translate_ocsp_query(*openssl_unknown), False)
|
||||
self.assertEqual(mock_log.debug.call_count, 1)
|
||||
self.assertEqual(mock_log.warn.call_count, 0)
|
||||
self.assertEqual(ocsp._translate_ocsp_query(*openssl_expired_ocsp), False)
|
||||
self.assertEqual(mock_log.debug.call_count, 2)
|
||||
self.assertEqual(ocsp._translate_ocsp_query(*openssl_broken), False)
|
||||
self.assertEqual(mock_log.warn.call_count, 1)
|
||||
mock_log.info.call_count = 0
|
||||
self.assertEqual(ocsp._translate_ocsp_query(*openssl_revoked), True)
|
||||
self.assertEqual(mock_log.info.call_count, 0)
|
||||
self.assertEqual(ocsp._translate_ocsp_query(*openssl_expired_ocsp_revoked), True)
|
||||
self.assertEqual(mock_log.info.call_count, 1)
|
||||
|
||||
|
||||
# pylint: disable=line-too-long
|
||||
openssl_confused = ("", """
|
||||
/etc/letsencrypt/live/example.org/cert.pem: good
|
||||
This Update: Dec 17 00:00:00 2016 GMT
|
||||
Next Update: Dec 24 00:00:00 2016 GMT
|
||||
""",
|
||||
"""
|
||||
Response Verify Failure
|
||||
139903674214048:error:27069065:OCSP routines:OCSP_basic_verify:certificate verify error:ocsp_vfy.c:138:Verify error:unable to get local issuer certificate
|
||||
""")
|
||||
|
||||
openssl_happy = ("blah.pem", """
|
||||
blah.pem: good
|
||||
This Update: Dec 20 18:00:00 2016 GMT
|
||||
Next Update: Dec 27 18:00:00 2016 GMT
|
||||
""",
|
||||
"Response verify OK")
|
||||
|
||||
openssl_revoked = ("blah.pem", """
|
||||
blah.pem: revoked
|
||||
This Update: Dec 20 01:00:00 2016 GMT
|
||||
Next Update: Dec 27 01:00:00 2016 GMT
|
||||
Revocation Time: Dec 20 01:46:34 2016 GMT
|
||||
""",
|
||||
"""Response verify OK""")
|
||||
|
||||
openssl_unknown = ("blah.pem", """
|
||||
blah.pem: unknown
|
||||
This Update: Dec 20 18:00:00 2016 GMT
|
||||
Next Update: Dec 27 18:00:00 2016 GMT
|
||||
""",
|
||||
"Response verify OK")
|
||||
|
||||
openssl_broken = ("", "tentacles", "Response verify OK")
|
||||
|
||||
openssl_expired_ocsp = ("blah.pem", """
|
||||
blah.pem: WARNING: Status times invalid.
|
||||
140659132298912:error:2707307D:OCSP routines:OCSP_check_validity:status expired:ocsp_cl.c:372:
|
||||
good
|
||||
This Update: Apr 6 00:00:00 2016 GMT
|
||||
Next Update: Apr 13 00:00:00 2016 GMT
|
||||
""",
|
||||
"""Response verify OK""")
|
||||
|
||||
openssl_expired_ocsp_revoked = ("blah.pem", """
|
||||
blah.pem: WARNING: Status times invalid.
|
||||
140659132298912:error:2707307D:OCSP routines:OCSP_check_validity:status expired:ocsp_cl.c:372:
|
||||
revoked
|
||||
This Update: Apr 6 00:00:00 2016 GMT
|
||||
Next Update: Apr 13 00:00:00 2016 GMT
|
||||
""",
|
||||
"""Response verify OK""")
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main() # pragma: no cover
|
||||
70
certbot/tests/renewal_test.py
Normal file
70
certbot/tests/renewal_test.py
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
"""Tests for certbot.renewal"""
|
||||
import os
|
||||
import mock
|
||||
import unittest
|
||||
import tempfile
|
||||
|
||||
from certbot import configuration
|
||||
from certbot import errors
|
||||
from certbot import storage
|
||||
|
||||
from certbot.tests import util
|
||||
|
||||
|
||||
class RenewalTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.tmp_dir = tempfile.mkdtemp()
|
||||
self.config_dir = os.path.join(self.tmp_dir, 'config')
|
||||
|
||||
@mock.patch('certbot.cli.set_by_cli')
|
||||
def test_ancient_webroot_renewal_conf(self, mock_set_by_cli):
|
||||
mock_set_by_cli.return_value = False
|
||||
rc_path = util.make_lineage(self, 'sample-renewal-ancient.conf')
|
||||
args = mock.MagicMock(account=None, email=None, webroot_path=None)
|
||||
config = configuration.NamespaceConfig(args)
|
||||
lineage = storage.RenewableCert(rc_path, config)
|
||||
renewalparams = lineage.configuration['renewalparams']
|
||||
# pylint: disable=protected-access
|
||||
from certbot import renewal
|
||||
renewal._restore_webroot_config(config, renewalparams)
|
||||
self.assertEqual(config.webroot_path, ['/var/www/'])
|
||||
|
||||
|
||||
class RestoreRequiredConfigElementsTest(unittest.TestCase):
|
||||
"""Tests for certbot.renewal.restore_required_config_elements."""
|
||||
def setUp(self):
|
||||
self.config = mock.MagicMock()
|
||||
|
||||
@classmethod
|
||||
def _call(cls, *args, **kwargs):
|
||||
from certbot.renewal import restore_required_config_elements
|
||||
return restore_required_config_elements(*args, **kwargs)
|
||||
|
||||
@mock.patch('certbot.renewal.cli.set_by_cli')
|
||||
def test_allow_subset_of_names_success(self, mock_set_by_cli):
|
||||
mock_set_by_cli.return_value = False
|
||||
self._call(self.config, {'allow_subset_of_names': 'True'})
|
||||
self.assertTrue(self.config.namespace.allow_subset_of_names is True)
|
||||
|
||||
@mock.patch('certbot.renewal.cli.set_by_cli')
|
||||
def test_allow_subset_of_names_failure(self, mock_set_by_cli):
|
||||
mock_set_by_cli.return_value = False
|
||||
renewalparams = {'allow_subset_of_names': 'maybe'}
|
||||
self.assertRaises(
|
||||
errors.Error, self._call, self.config, renewalparams)
|
||||
|
||||
@mock.patch('certbot.renewal.cli.set_by_cli')
|
||||
def test_must_staple_success(self, mock_set_by_cli):
|
||||
mock_set_by_cli.return_value = False
|
||||
self._call(self.config, {'must_staple': 'True'})
|
||||
self.assertTrue(self.config.namespace.must_staple is True)
|
||||
|
||||
@mock.patch('certbot.renewal.cli.set_by_cli')
|
||||
def test_must_staple_failure(self, mock_set_by_cli):
|
||||
mock_set_by_cli.return_value = False
|
||||
renewalparams = {'must_staple': 'maybe'}
|
||||
self.assertRaises(
|
||||
errors.Error, self._call, self.config, renewalparams)
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main() # pragma: no cover
|
||||
|
|
@ -8,7 +8,6 @@ import six
|
|||
|
||||
class ReporterTest(unittest.TestCase):
|
||||
"""Tests for certbot.reporter.Reporter."""
|
||||
|
||||
def setUp(self):
|
||||
from certbot import reporter
|
||||
self.reporter = reporter.Reporter(mock.MagicMock(quiet=False))
|
||||
|
|
@ -21,7 +20,7 @@ class ReporterTest(unittest.TestCase):
|
|||
|
||||
def test_multiline_message(self):
|
||||
self.reporter.add_message("Line 1\nLine 2", self.reporter.LOW_PRIORITY)
|
||||
self.reporter.atexit_print_messages()
|
||||
self.reporter.print_messages()
|
||||
output = sys.stdout.getvalue()
|
||||
self.assertTrue("Line 1\n" in output)
|
||||
self.assertTrue("Line 2" in output)
|
||||
|
|
@ -39,9 +38,12 @@ class ReporterTest(unittest.TestCase):
|
|||
self.reporter.print_messages()
|
||||
self.assertEqual(sys.stdout.getvalue(), "")
|
||||
|
||||
def test_atexit_print_messages(self):
|
||||
@mock.patch('certbot.reporter.os.getpid')
|
||||
def test_atexit_print_messages(self, mock_getpid):
|
||||
self._add_messages()
|
||||
self.reporter.atexit_print_messages()
|
||||
mock_getpid.return_value = 42
|
||||
with mock.patch('certbot.reporter.INITIAL_PID', 42):
|
||||
self.reporter.atexit_print_messages()
|
||||
output = sys.stdout.getvalue()
|
||||
self.assertTrue("IMPORTANT NOTES:" in output)
|
||||
self.assertTrue("High" in output)
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ import six
|
|||
|
||||
from certbot import errors
|
||||
|
||||
from certbot.tests import util as test_util
|
||||
|
||||
|
||||
class ReverterCheckpointLocalTest(unittest.TestCase):
|
||||
# pylint: disable=too-many-instance-attributes, too-many-public-methods
|
||||
|
|
@ -375,7 +377,7 @@ class TestFullCheckpointsReverter(unittest.TestCase):
|
|||
self.assertEqual(read_in(self.config2), "directive-dir2")
|
||||
self.assertFalse(os.path.isfile(config3))
|
||||
|
||||
@mock.patch("certbot.reverter.zope.component.getUtility")
|
||||
@test_util.patch_get_utility()
|
||||
def test_view_config_changes(self, mock_output):
|
||||
"""This is not strict as this is subject to change."""
|
||||
self._setup_three_checkpoints()
|
||||
|
|
|
|||
|
|
@ -17,10 +17,10 @@ from certbot import configuration
|
|||
from certbot import errors
|
||||
from certbot.storage import ALL_FOUR
|
||||
|
||||
from certbot.tests import test_util
|
||||
from certbot.tests import util
|
||||
|
||||
|
||||
CERT = test_util.load_cert('cert.pem')
|
||||
CERT = util.load_cert('cert.pem')
|
||||
|
||||
|
||||
def unlink_all(rc_object):
|
||||
|
|
@ -43,11 +43,13 @@ class BaseRenewableCertTest(unittest.TestCase):
|
|||
your test. Check :class:`.cli_test.DuplicateCertTest` for an example.
|
||||
|
||||
"""
|
||||
_multiprocess_can_split_ = True
|
||||
|
||||
def setUp(self):
|
||||
from certbot import storage
|
||||
self.tempdir = tempfile.mkdtemp()
|
||||
|
||||
self.cli_config = configuration.RenewerConfiguration(
|
||||
self.cli_config = configuration.NamespaceConfig(
|
||||
namespace=mock.MagicMock(
|
||||
config_dir=self.tempdir,
|
||||
work_dir=self.tempdir,
|
||||
|
|
@ -55,16 +57,22 @@ class BaseRenewableCertTest(unittest.TestCase):
|
|||
)
|
||||
)
|
||||
|
||||
# TODO: maybe provide RenewerConfiguration.make_dirs?
|
||||
# TODO: maybe provide NamespaceConfig.make_dirs?
|
||||
# TODO: main() should create those dirs, c.f. #902
|
||||
os.makedirs(os.path.join(self.tempdir, "live", "example.org"))
|
||||
os.makedirs(os.path.join(self.tempdir, "archive", "example.org"))
|
||||
archive_path = os.path.join(self.tempdir, "archive", "example.org")
|
||||
os.makedirs(archive_path)
|
||||
os.makedirs(os.path.join(self.tempdir, "renewal"))
|
||||
|
||||
config = configobj.ConfigObj()
|
||||
for kind in ALL_FOUR:
|
||||
config[kind] = os.path.join(self.tempdir, "live", "example.org",
|
||||
kind_path = os.path.join(self.tempdir, "live", "example.org",
|
||||
kind + ".pem")
|
||||
config[kind] = kind_path
|
||||
with open(os.path.join(self.tempdir, "live", "example.org",
|
||||
"README"), 'a'):
|
||||
pass
|
||||
config["archive"] = archive_path
|
||||
config.filename = os.path.join(self.tempdir, "renewal",
|
||||
"example.org.conf")
|
||||
config.write()
|
||||
|
|
@ -363,18 +371,18 @@ class RenewableCertTests(BaseRenewableCertTest):
|
|||
|
||||
def test_names(self):
|
||||
# Trying the current version
|
||||
self._write_out_kind("cert", 12, test_util.load_vector("cert-san.pem"))
|
||||
self._write_out_kind("cert", 12, util.load_vector("cert-san.pem"))
|
||||
self.assertEqual(self.test_rc.names(),
|
||||
["example.com", "www.example.com"])
|
||||
|
||||
# Trying a non-current version
|
||||
self._write_out_kind("cert", 15, test_util.load_vector("cert.pem"))
|
||||
self._write_out_kind("cert", 15, util.load_vector("cert.pem"))
|
||||
self.assertEqual(self.test_rc.names(12),
|
||||
["example.com", "www.example.com"])
|
||||
|
||||
# Testing common name is listed first
|
||||
self._write_out_kind(
|
||||
"cert", 12, test_util.load_vector("cert-5sans.pem"))
|
||||
"cert", 12, util.load_vector("cert-5sans.pem"))
|
||||
self.assertEqual(
|
||||
self.test_rc.names(12),
|
||||
["example.com"] + ["{0}.example.com".format(c) for c in "abcd"])
|
||||
|
|
@ -387,7 +395,7 @@ class RenewableCertTests(BaseRenewableCertTest):
|
|||
def test_time_interval_judgments(self, mock_datetime):
|
||||
"""Test should_autodeploy() and should_autorenew() on the basis
|
||||
of expiry time windows."""
|
||||
test_cert = test_util.load_vector("cert.pem")
|
||||
test_cert = util.load_vector("cert.pem")
|
||||
self._write_out_ex_kinds()
|
||||
|
||||
self.test_rc.update_all_links_to(12)
|
||||
|
|
@ -543,7 +551,8 @@ class RenewableCertTests(BaseRenewableCertTest):
|
|||
|
||||
from certbot.storage import relevant_values
|
||||
with mock.patch("certbot.cli.helpful_parser", mock_parser):
|
||||
return relevant_values(values)
|
||||
# make a copy to ensure values isn't modified
|
||||
return relevant_values(values.copy())
|
||||
|
||||
def test_relevant_values(self):
|
||||
"""Test that relevant_values() can reject an irrelevant value."""
|
||||
|
|
@ -559,10 +568,18 @@ class RenewableCertTests(BaseRenewableCertTest):
|
|||
def test_relevant_values_nondefault(self):
|
||||
"""Test that relevant_values() can retain a non-default value."""
|
||||
values = {"rsa_key_size": 12}
|
||||
# A copy is given to _test_relevant_values_common
|
||||
# to make sure values isn't modified by the method
|
||||
self.assertEqual(
|
||||
self._test_relevant_values_common(values.copy()), values)
|
||||
self._test_relevant_values_common(values), values)
|
||||
|
||||
def test_relevant_values_bool(self):
|
||||
values = {"allow_subset_of_names": True}
|
||||
self.assertEqual(
|
||||
self._test_relevant_values_common(values), values)
|
||||
|
||||
def test_relevant_values_str(self):
|
||||
values = {"authenticator": "apache"}
|
||||
self.assertEqual(
|
||||
self._test_relevant_values_common(values), values)
|
||||
|
||||
@mock.patch("certbot.storage.relevant_values")
|
||||
def test_new_lineage(self, mock_rv):
|
||||
|
|
@ -573,34 +590,38 @@ class RenewableCertTests(BaseRenewableCertTest):
|
|||
|
||||
from certbot import storage
|
||||
result = storage.RenewableCert.new_lineage(
|
||||
"the-lineage.com", "cert", "privkey", "chain", self.cli_config)
|
||||
"the-lineage.com", b"cert", b"privkey", b"chain", self.cli_config)
|
||||
# This consistency check tests most relevant properties about the
|
||||
# newly created cert lineage.
|
||||
# pylint: disable=protected-access
|
||||
self.assertTrue(result._consistent())
|
||||
self.assertTrue(os.path.exists(os.path.join(
|
||||
self.cli_config.renewal_configs_dir, "the-lineage.com.conf")))
|
||||
with open(result.fullchain) as f:
|
||||
self.assertEqual(f.read(), "cert" + "chain")
|
||||
self.assertTrue(os.path.exists(os.path.join(
|
||||
self.cli_config.live_dir, "the-lineage.com", "README")))
|
||||
with open(result.fullchain, "rb") as f:
|
||||
self.assertEqual(f.read(), b"cert" + b"chain")
|
||||
# Let's do it again and make sure it makes a different lineage
|
||||
result = storage.RenewableCert.new_lineage(
|
||||
"the-lineage.com", "cert2", "privkey2", "chain2", self.cli_config)
|
||||
"the-lineage.com", b"cert2", b"privkey2", b"chain2", self.cli_config)
|
||||
self.assertTrue(os.path.exists(os.path.join(
|
||||
self.cli_config.renewal_configs_dir, "the-lineage.com-0001.conf")))
|
||||
self.assertTrue(os.path.exists(os.path.join(
|
||||
self.cli_config.live_dir, "the-lineage.com-0001", "README")))
|
||||
# Now trigger the detection of already existing files
|
||||
os.mkdir(os.path.join(
|
||||
self.cli_config.live_dir, "the-lineage.com-0002"))
|
||||
self.assertRaises(errors.CertStorageError,
|
||||
storage.RenewableCert.new_lineage, "the-lineage.com",
|
||||
"cert3", "privkey3", "chain3", self.cli_config)
|
||||
os.mkdir(os.path.join(self.cli_config.archive_dir, "other-example.com"))
|
||||
b"cert3", b"privkey3", b"chain3", self.cli_config)
|
||||
os.mkdir(os.path.join(self.cli_config.default_archive_dir, "other-example.com"))
|
||||
self.assertRaises(errors.CertStorageError,
|
||||
storage.RenewableCert.new_lineage,
|
||||
"other-example.com", "cert4",
|
||||
"privkey4", "chain4", self.cli_config)
|
||||
"other-example.com", b"cert4",
|
||||
b"privkey4", b"chain4", self.cli_config)
|
||||
# Make sure it can accept renewal parameters
|
||||
result = storage.RenewableCert.new_lineage(
|
||||
"the-lineage.com", "cert2", "privkey2", "chain2", self.cli_config)
|
||||
"the-lineage.com", b"cert2", b"privkey2", b"chain2", self.cli_config)
|
||||
# TODO: Conceivably we could test that the renewal parameters actually
|
||||
# got saved
|
||||
|
||||
|
|
@ -613,18 +634,18 @@ class RenewableCertTests(BaseRenewableCertTest):
|
|||
|
||||
from certbot import storage
|
||||
shutil.rmtree(self.cli_config.renewal_configs_dir)
|
||||
shutil.rmtree(self.cli_config.archive_dir)
|
||||
shutil.rmtree(self.cli_config.default_archive_dir)
|
||||
shutil.rmtree(self.cli_config.live_dir)
|
||||
|
||||
storage.RenewableCert.new_lineage(
|
||||
"the-lineage.com", "cert2", "privkey2", "chain2", self.cli_config)
|
||||
"the-lineage.com", b"cert2", b"privkey2", b"chain2", self.cli_config)
|
||||
self.assertTrue(os.path.exists(
|
||||
os.path.join(
|
||||
self.cli_config.renewal_configs_dir, "the-lineage.com.conf")))
|
||||
self.assertTrue(os.path.exists(os.path.join(
|
||||
self.cli_config.live_dir, "the-lineage.com", "privkey.pem")))
|
||||
self.assertTrue(os.path.exists(os.path.join(
|
||||
self.cli_config.archive_dir, "the-lineage.com", "privkey1.pem")))
|
||||
self.cli_config.default_archive_dir, "the-lineage.com", "privkey1.pem")))
|
||||
|
||||
@mock.patch("certbot.storage.util.unique_lineage_name")
|
||||
def test_invalid_config_filename(self, mock_uln):
|
||||
|
|
@ -699,6 +720,19 @@ class RenewableCertTests(BaseRenewableCertTest):
|
|||
self.assertEqual(storage.add_time_interval(base_time, interval),
|
||||
excepted)
|
||||
|
||||
def test_is_test_cert(self):
|
||||
self.test_rc.configuration["renewalparams"] = {}
|
||||
rp = self.test_rc.configuration["renewalparams"]
|
||||
self.assertEqual(self.test_rc.is_test_cert, False)
|
||||
rp["server"] = "https://acme-staging.api.letsencrypt.org/directory"
|
||||
self.assertEqual(self.test_rc.is_test_cert, True)
|
||||
rp["server"] = "https://staging.someotherca.com/directory"
|
||||
self.assertEqual(self.test_rc.is_test_cert, True)
|
||||
rp["server"] = "https://acme-v01.api.letsencrypt.org/directory"
|
||||
self.assertEqual(self.test_rc.is_test_cert, False)
|
||||
rp["server"] = "https://acme-v02.api.letsencrypt.org/directory"
|
||||
self.assertEqual(self.test_rc.is_test_cert, False)
|
||||
|
||||
def test_missing_cert(self):
|
||||
from certbot import storage
|
||||
self.assertRaises(errors.CertStorageError,
|
||||
|
|
@ -721,9 +755,10 @@ class RenewableCertTests(BaseRenewableCertTest):
|
|||
target = {}
|
||||
for x in ALL_FOUR:
|
||||
target[x] = "somewhere"
|
||||
archive_dir = "the_archive"
|
||||
relevant_data = {"useful": "new_value"}
|
||||
from certbot import storage
|
||||
storage.write_renewal_config(temp, temp2, target, relevant_data)
|
||||
storage.write_renewal_config(temp, temp2, archive_dir, target, relevant_data)
|
||||
with open(temp2, "r") as f:
|
||||
content = f.read()
|
||||
# useful value was updated
|
||||
|
|
@ -735,6 +770,110 @@ class RenewableCertTests(BaseRenewableCertTest):
|
|||
# check version was stored
|
||||
self.assertTrue("version = {0}".format(certbot.__version__) in content)
|
||||
|
||||
def test_update_symlinks(self):
|
||||
from certbot import storage
|
||||
archive_dir_path = os.path.join(self.tempdir, "archive", "example.org")
|
||||
for kind in ALL_FOUR:
|
||||
live_path = self.config[kind]
|
||||
basename = kind + "1.pem"
|
||||
archive_path = os.path.join(archive_dir_path, basename)
|
||||
open(archive_path, 'a').close()
|
||||
os.symlink(os.path.join(self.tempdir, basename), live_path)
|
||||
self.assertRaises(errors.CertStorageError,
|
||||
storage.RenewableCert, self.config.filename,
|
||||
self.cli_config)
|
||||
storage.RenewableCert(self.config.filename, self.cli_config,
|
||||
update_symlinks=True)
|
||||
|
||||
class DeleteFilesTest(BaseRenewableCertTest):
|
||||
"""Tests for certbot.storage.delete_files"""
|
||||
def setUp(self):
|
||||
super(DeleteFilesTest, self).setUp()
|
||||
for kind in ALL_FOUR:
|
||||
kind_path = os.path.join(self.tempdir, "live", "example.org",
|
||||
kind + ".pem")
|
||||
with open(kind_path, 'a'):
|
||||
pass
|
||||
self.config.write()
|
||||
self.assertTrue(os.path.exists(os.path.join(
|
||||
self.cli_config.renewal_configs_dir, "example.org.conf")))
|
||||
self.assertTrue(os.path.exists(os.path.join(
|
||||
self.cli_config.live_dir, "example.org")))
|
||||
self.assertTrue(os.path.exists(os.path.join(
|
||||
self.tempdir, "archive", "example.org")))
|
||||
|
||||
def _call(self):
|
||||
from certbot import storage
|
||||
with mock.patch("certbot.storage.logger"):
|
||||
storage.delete_files(self.cli_config, "example.org")
|
||||
|
||||
def test_delete_all_files(self):
|
||||
self._call()
|
||||
|
||||
self.assertFalse(os.path.exists(os.path.join(
|
||||
self.cli_config.renewal_configs_dir, "example.org.conf")))
|
||||
self.assertFalse(os.path.exists(os.path.join(
|
||||
self.cli_config.live_dir, "example.org")))
|
||||
self.assertFalse(os.path.exists(os.path.join(
|
||||
self.tempdir, "archive", "example.org")))
|
||||
|
||||
def test_bad_renewal_config(self):
|
||||
with open(self.config.filename, 'a') as config_file:
|
||||
config_file.write("asdfasfasdfasdf")
|
||||
|
||||
self.assertRaises(errors.CertStorageError, self._call)
|
||||
self.assertTrue(os.path.exists(os.path.join(
|
||||
self.cli_config.live_dir, "example.org")))
|
||||
self.assertFalse(os.path.exists(os.path.join(
|
||||
self.cli_config.renewal_configs_dir, "example.org.conf")))
|
||||
|
||||
def test_no_renewal_config(self):
|
||||
os.remove(self.config.filename)
|
||||
self.assertRaises(errors.CertStorageError, self._call)
|
||||
self.assertTrue(os.path.exists(os.path.join(
|
||||
self.cli_config.live_dir, "example.org")))
|
||||
self.assertFalse(os.path.exists(self.config.filename))
|
||||
|
||||
def test_no_cert_file(self):
|
||||
os.remove(os.path.join(
|
||||
self.cli_config.live_dir, "example.org", "cert.pem"))
|
||||
self._call()
|
||||
self.assertFalse(os.path.exists(self.config.filename))
|
||||
self.assertFalse(os.path.exists(os.path.join(
|
||||
self.cli_config.live_dir, "example.org")))
|
||||
self.assertFalse(os.path.exists(os.path.join(
|
||||
self.tempdir, "archive", "example.org")))
|
||||
|
||||
def test_no_readme_file(self):
|
||||
os.remove(os.path.join(
|
||||
self.cli_config.live_dir, "example.org", "README"))
|
||||
self._call()
|
||||
self.assertFalse(os.path.exists(self.config.filename))
|
||||
self.assertFalse(os.path.exists(os.path.join(
|
||||
self.cli_config.live_dir, "example.org")))
|
||||
self.assertFalse(os.path.exists(os.path.join(
|
||||
self.tempdir, "archive", "example.org")))
|
||||
|
||||
def test_livedir_not_empty(self):
|
||||
with open(os.path.join(
|
||||
self.cli_config.live_dir, "example.org", "other_file"), 'a'):
|
||||
pass
|
||||
self._call()
|
||||
self.assertFalse(os.path.exists(self.config.filename))
|
||||
self.assertTrue(os.path.exists(os.path.join(
|
||||
self.cli_config.live_dir, "example.org")))
|
||||
self.assertFalse(os.path.exists(os.path.join(
|
||||
self.tempdir, "archive", "example.org")))
|
||||
|
||||
def test_no_archive(self):
|
||||
archive_dir = os.path.join(self.tempdir, "archive", "example.org")
|
||||
os.rmdir(archive_dir)
|
||||
self._call()
|
||||
self.assertFalse(os.path.exists(self.config.filename))
|
||||
self.assertFalse(os.path.exists(os.path.join(
|
||||
self.cli_config.live_dir, "example.org")))
|
||||
self.assertFalse(os.path.exists(archive_dir))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main() # pragma: no cover
|
||||
|
|
|
|||
|
|
@ -1,113 +0,0 @@
|
|||
"""Test utilities.
|
||||
|
||||
.. warning:: This module is not part of the public API.
|
||||
|
||||
"""
|
||||
import os
|
||||
import pkg_resources
|
||||
import unittest
|
||||
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
import OpenSSL
|
||||
|
||||
from acme import errors
|
||||
from acme import jose
|
||||
from acme import util
|
||||
|
||||
|
||||
def vector_path(*names):
|
||||
"""Path to a test vector."""
|
||||
return pkg_resources.resource_filename(
|
||||
__name__, os.path.join('testdata', *names))
|
||||
|
||||
|
||||
def load_vector(*names):
|
||||
"""Load contents of a test vector."""
|
||||
# luckily, resource_string opens file in binary mode
|
||||
return pkg_resources.resource_string(
|
||||
__name__, os.path.join('testdata', *names))
|
||||
|
||||
|
||||
def _guess_loader(filename, loader_pem, loader_der):
|
||||
_, ext = os.path.splitext(filename)
|
||||
if ext.lower() == '.pem':
|
||||
return loader_pem
|
||||
elif ext.lower() == '.der':
|
||||
return loader_der
|
||||
else: # pragma: no cover
|
||||
raise ValueError("Loader could not be recognized based on extension")
|
||||
|
||||
|
||||
def load_cert(*names):
|
||||
"""Load certificate."""
|
||||
loader = _guess_loader(
|
||||
names[-1], OpenSSL.crypto.FILETYPE_PEM, OpenSSL.crypto.FILETYPE_ASN1)
|
||||
return OpenSSL.crypto.load_certificate(loader, load_vector(*names))
|
||||
|
||||
|
||||
def load_comparable_cert(*names):
|
||||
"""Load ComparableX509 cert."""
|
||||
return jose.ComparableX509(load_cert(*names))
|
||||
|
||||
|
||||
def load_csr(*names):
|
||||
"""Load certificate request."""
|
||||
loader = _guess_loader(
|
||||
names[-1], OpenSSL.crypto.FILETYPE_PEM, OpenSSL.crypto.FILETYPE_ASN1)
|
||||
return OpenSSL.crypto.load_certificate_request(loader, load_vector(*names))
|
||||
|
||||
|
||||
def load_comparable_csr(*names):
|
||||
"""Load ComparableX509 certificate request."""
|
||||
return jose.ComparableX509(load_csr(*names))
|
||||
|
||||
|
||||
def load_rsa_private_key(*names):
|
||||
"""Load RSA private key."""
|
||||
loader = _guess_loader(names[-1], serialization.load_pem_private_key,
|
||||
serialization.load_der_private_key)
|
||||
return jose.ComparableRSAKey(loader(
|
||||
load_vector(*names), password=None, backend=default_backend()))
|
||||
|
||||
|
||||
def load_pyopenssl_private_key(*names):
|
||||
"""Load pyOpenSSL private key."""
|
||||
loader = _guess_loader(
|
||||
names[-1], OpenSSL.crypto.FILETYPE_PEM, OpenSSL.crypto.FILETYPE_ASN1)
|
||||
return OpenSSL.crypto.load_privatekey(loader, load_vector(*names))
|
||||
|
||||
|
||||
def requirement_available(requirement):
|
||||
"""Checks if requirement can be imported.
|
||||
|
||||
:rtype: bool
|
||||
:returns: ``True`` iff requirement can be imported
|
||||
|
||||
"""
|
||||
try:
|
||||
util.activate(requirement)
|
||||
except errors.DependencyError: # pragma: no cover
|
||||
return False
|
||||
return True # pragma: no cover
|
||||
|
||||
|
||||
def skip_unless(condition, reason): # pragma: no cover
|
||||
"""Skip tests unless a condition holds.
|
||||
|
||||
This implements the basic functionality of unittest.skipUnless
|
||||
which is only available on Python 2.7+.
|
||||
|
||||
:param bool condition: If ``False``, the test will be skipped
|
||||
:param str reason: the reason for skipping the test
|
||||
|
||||
:rtype: callable
|
||||
:returns: decorator that hides tests unless condition is ``True``
|
||||
|
||||
"""
|
||||
if hasattr(unittest, "skipUnless"):
|
||||
return unittest.skipUnless(condition, reason)
|
||||
elif condition:
|
||||
return lambda cls: cls
|
||||
else:
|
||||
return lambda cls: None
|
||||
1
certbot/tests/testdata/sample-renewal.conf
vendored
1
certbot/tests/testdata/sample-renewal.conf
vendored
|
|
@ -73,4 +73,5 @@ tls_sni_01_port = 443
|
|||
logs_dir = /var/log/letsencrypt
|
||||
apache_vhost_root = /etc/apache2/sites-available
|
||||
configurator = None
|
||||
must_staple = True
|
||||
[[webroot_map]]
|
||||
|
|
|
|||
248
certbot/tests/util.py
Normal file
248
certbot/tests/util.py
Normal file
|
|
@ -0,0 +1,248 @@
|
|||
"""Test utilities.
|
||||
|
||||
.. warning:: This module is not part of the public API.
|
||||
|
||||
"""
|
||||
import os
|
||||
import pkg_resources
|
||||
import shutil
|
||||
import unittest
|
||||
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
import mock
|
||||
import OpenSSL
|
||||
|
||||
from acme import errors
|
||||
from acme import jose
|
||||
from acme import util
|
||||
|
||||
from certbot import constants
|
||||
from certbot import interfaces
|
||||
from certbot import storage
|
||||
|
||||
from certbot.display import util as display_util
|
||||
|
||||
|
||||
def vector_path(*names):
|
||||
"""Path to a test vector."""
|
||||
return pkg_resources.resource_filename(
|
||||
__name__, os.path.join('testdata', *names))
|
||||
|
||||
|
||||
def load_vector(*names):
|
||||
"""Load contents of a test vector."""
|
||||
# luckily, resource_string opens file in binary mode
|
||||
return pkg_resources.resource_string(
|
||||
__name__, os.path.join('testdata', *names))
|
||||
|
||||
|
||||
def _guess_loader(filename, loader_pem, loader_der):
|
||||
_, ext = os.path.splitext(filename)
|
||||
if ext.lower() == '.pem':
|
||||
return loader_pem
|
||||
elif ext.lower() == '.der':
|
||||
return loader_der
|
||||
else: # pragma: no cover
|
||||
raise ValueError("Loader could not be recognized based on extension")
|
||||
|
||||
|
||||
def load_cert(*names):
|
||||
"""Load certificate."""
|
||||
loader = _guess_loader(
|
||||
names[-1], OpenSSL.crypto.FILETYPE_PEM, OpenSSL.crypto.FILETYPE_ASN1)
|
||||
return OpenSSL.crypto.load_certificate(loader, load_vector(*names))
|
||||
|
||||
|
||||
def load_comparable_cert(*names):
|
||||
"""Load ComparableX509 cert."""
|
||||
return jose.ComparableX509(load_cert(*names))
|
||||
|
||||
|
||||
def load_csr(*names):
|
||||
"""Load certificate request."""
|
||||
loader = _guess_loader(
|
||||
names[-1], OpenSSL.crypto.FILETYPE_PEM, OpenSSL.crypto.FILETYPE_ASN1)
|
||||
return OpenSSL.crypto.load_certificate_request(loader, load_vector(*names))
|
||||
|
||||
|
||||
def load_comparable_csr(*names):
|
||||
"""Load ComparableX509 certificate request."""
|
||||
return jose.ComparableX509(load_csr(*names))
|
||||
|
||||
|
||||
def load_rsa_private_key(*names):
|
||||
"""Load RSA private key."""
|
||||
loader = _guess_loader(names[-1], serialization.load_pem_private_key,
|
||||
serialization.load_der_private_key)
|
||||
return jose.ComparableRSAKey(loader(
|
||||
load_vector(*names), password=None, backend=default_backend()))
|
||||
|
||||
|
||||
def load_pyopenssl_private_key(*names):
|
||||
"""Load pyOpenSSL private key."""
|
||||
loader = _guess_loader(
|
||||
names[-1], OpenSSL.crypto.FILETYPE_PEM, OpenSSL.crypto.FILETYPE_ASN1)
|
||||
return OpenSSL.crypto.load_privatekey(loader, load_vector(*names))
|
||||
|
||||
|
||||
def requirement_available(requirement):
|
||||
"""Checks if requirement can be imported.
|
||||
|
||||
:rtype: bool
|
||||
:returns: ``True`` iff requirement can be imported
|
||||
|
||||
"""
|
||||
try:
|
||||
util.activate(requirement)
|
||||
except errors.DependencyError: # pragma: no cover
|
||||
return False
|
||||
return True # pragma: no cover
|
||||
|
||||
|
||||
def skip_unless(condition, reason): # pragma: no cover
|
||||
"""Skip tests unless a condition holds.
|
||||
|
||||
This implements the basic functionality of unittest.skipUnless
|
||||
which is only available on Python 2.7+.
|
||||
|
||||
:param bool condition: If ``False``, the test will be skipped
|
||||
:param str reason: the reason for skipping the test
|
||||
|
||||
:rtype: callable
|
||||
:returns: decorator that hides tests unless condition is ``True``
|
||||
|
||||
"""
|
||||
if hasattr(unittest, "skipUnless"):
|
||||
return unittest.skipUnless(condition, reason)
|
||||
elif condition:
|
||||
return lambda cls: cls
|
||||
else:
|
||||
return lambda cls: None
|
||||
|
||||
|
||||
def make_lineage(self, testfile):
|
||||
"""Creates a lineage defined by testfile.
|
||||
|
||||
This creates the archive, live, and renewal directories if
|
||||
necessary and creates a simple lineage.
|
||||
|
||||
:param str testfile: configuration file to base the lineage on
|
||||
|
||||
:returns: path to the renewal conf file for the created lineage
|
||||
:rtype: str
|
||||
|
||||
"""
|
||||
lineage_name = testfile[:-len('.conf')]
|
||||
|
||||
conf_dir = os.path.join(
|
||||
self.config_dir, constants.RENEWAL_CONFIGS_DIR)
|
||||
archive_dir = os.path.join(
|
||||
self.config_dir, constants.ARCHIVE_DIR, lineage_name)
|
||||
live_dir = os.path.join(
|
||||
self.config_dir, constants.LIVE_DIR, lineage_name)
|
||||
|
||||
for directory in (archive_dir, conf_dir, live_dir,):
|
||||
if not os.path.exists(directory):
|
||||
os.makedirs(directory)
|
||||
|
||||
sample_archive = vector_path('sample-archive')
|
||||
for kind in os.listdir(sample_archive):
|
||||
shutil.copyfile(os.path.join(sample_archive, kind),
|
||||
os.path.join(archive_dir, kind))
|
||||
|
||||
for kind in storage.ALL_FOUR:
|
||||
os.symlink(os.path.join(archive_dir, '{0}1.pem'.format(kind)),
|
||||
os.path.join(live_dir, '{0}.pem'.format(kind)))
|
||||
|
||||
conf_path = os.path.join(self.config_dir, conf_dir, testfile)
|
||||
with open(vector_path(testfile)) as src:
|
||||
with open(conf_path, 'w') as dst:
|
||||
dst.writelines(
|
||||
line.replace('MAGICDIR', self.config_dir) for line in src)
|
||||
|
||||
return conf_path
|
||||
|
||||
|
||||
def patch_get_utility(target='zope.component.getUtility'):
|
||||
"""Patch zope.component.getUtility to use a special mock IDisplay.
|
||||
|
||||
The mock IDisplay works like a regular mock object, except it also
|
||||
also asserts that methods are called with valid arguments.
|
||||
|
||||
:param str target: path to patch
|
||||
|
||||
:returns: mock zope.component.getUtility
|
||||
:rtype: mock.MagicMock
|
||||
|
||||
"""
|
||||
return mock.patch(target, new_callable=_create_get_utility_mock)
|
||||
|
||||
|
||||
class FreezableMock(object):
|
||||
"""Mock object with the ability to freeze attributes.
|
||||
|
||||
This class works like a regular mock.MagicMock object, except
|
||||
attributes and behavior can be set and frozen so they cannot be
|
||||
changed during tests.
|
||||
|
||||
If a func argument is provided to the constructor, this function
|
||||
is called first when an instance of FreezableMock is called,
|
||||
followed by the usual behavior defined by MagicMock. The return
|
||||
value of func is ignored.
|
||||
|
||||
"""
|
||||
def __init__(self, frozen=False, func=None):
|
||||
self._frozen_set = set() if frozen else set(('freeze',))
|
||||
self._func = func
|
||||
self._mock = mock.MagicMock()
|
||||
self._frozen = frozen
|
||||
|
||||
def freeze(self):
|
||||
"""Freeze object preventing further changes."""
|
||||
self._frozen = True
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
if self._func is not None:
|
||||
self._func(*args, **kwargs)
|
||||
return self._mock(*args, **kwargs)
|
||||
|
||||
def __getattribute__(self, name):
|
||||
if name == '_frozen':
|
||||
try:
|
||||
return object.__getattribute__(self, name)
|
||||
except AttributeError:
|
||||
return False
|
||||
elif name == '_frozen_set' or name in self._frozen_set:
|
||||
return object.__getattribute__(self, name)
|
||||
else:
|
||||
return getattr(object.__getattribute__(self, '_mock'), name)
|
||||
|
||||
def __setattr__(self, name, value):
|
||||
if self._frozen:
|
||||
return setattr(self._mock, name, value)
|
||||
elif name != '_frozen_set':
|
||||
self._frozen_set.add(name)
|
||||
return object.__setattr__(self, name, value)
|
||||
|
||||
|
||||
def _create_get_utility_mock():
|
||||
display = FreezableMock()
|
||||
for name in interfaces.IDisplay.names(): # pylint: disable=no-member
|
||||
if name != 'notification':
|
||||
frozen_mock = FreezableMock(frozen=True, func=_assert_valid_call)
|
||||
setattr(display, name, frozen_mock)
|
||||
display.freeze()
|
||||
return mock.MagicMock(return_value=display)
|
||||
|
||||
|
||||
def _assert_valid_call(*args, **kwargs):
|
||||
assert_args = [args[0] if args else kwargs['message']]
|
||||
|
||||
assert_kwargs = {}
|
||||
assert_kwargs['default'] = kwargs.get('default', None)
|
||||
assert_kwargs['cli_flag'] = kwargs.get('cli_flag', None)
|
||||
assert_kwargs['force_interactive'] = kwargs.get('force_interactive', False)
|
||||
|
||||
# pylint: disable=star-args
|
||||
display_util.assert_valid_call(*assert_args, **assert_kwargs)
|
||||
|
|
@ -11,7 +11,7 @@ import mock
|
|||
import six
|
||||
|
||||
from certbot import errors
|
||||
from certbot.tests import test_util
|
||||
import certbot.tests.util as test_util
|
||||
|
||||
|
||||
class RunScriptTest(unittest.TestCase):
|
||||
|
|
@ -195,6 +195,7 @@ except NameError:
|
|||
import io
|
||||
file_type = io.TextIOWrapper
|
||||
|
||||
|
||||
class UniqueLineageNameTest(unittest.TestCase):
|
||||
"""Tests for certbot.util.unique_lineage_name."""
|
||||
|
||||
|
|
@ -373,6 +374,49 @@ class EnforceDomainSanityTest(unittest.TestCase):
|
|||
self.assertRaises(errors.ConfigurationError, self._call,
|
||||
u"eichh\u00f6rnchen.example.com")
|
||||
|
||||
def test_too_long(self):
|
||||
long_domain = u"a"*256
|
||||
self.assertRaises(errors.ConfigurationError, self._call,
|
||||
long_domain)
|
||||
|
||||
def test_not_too_long(self):
|
||||
not_too_long_domain = u"{0}.{1}.{2}.{3}".format("a"*63, "b"*63, "c"*63, "d"*63)
|
||||
self._call(not_too_long_domain)
|
||||
|
||||
def test_empty_label(self):
|
||||
empty_label_domain = u"fizz..example.com"
|
||||
self.assertRaises(errors.ConfigurationError, self._call,
|
||||
empty_label_domain)
|
||||
|
||||
def test_empty_trailing_label(self):
|
||||
empty_trailing_label_domain = u"example.com.."
|
||||
self.assertRaises(errors.ConfigurationError, self._call,
|
||||
empty_trailing_label_domain)
|
||||
|
||||
def test_long_label_1(self):
|
||||
long_label_domain = u"a"*64
|
||||
self.assertRaises(errors.ConfigurationError, self._call,
|
||||
long_label_domain)
|
||||
|
||||
def test_long_label_2(self):
|
||||
long_label_domain = u"{0}.{1}.com".format(u"a"*64, u"b"*63)
|
||||
self.assertRaises(errors.ConfigurationError, self._call,
|
||||
long_label_domain)
|
||||
|
||||
def test_not_long_label(self):
|
||||
not_too_long_label_domain = u"{0}.{1}.com".format(u"a"*63, u"b"*63)
|
||||
self._call(not_too_long_label_domain)
|
||||
|
||||
def test_empty_domain(self):
|
||||
empty_domain = u""
|
||||
self.assertRaises(errors.ConfigurationError, self._call,
|
||||
empty_domain)
|
||||
|
||||
def test_punycode_ok(self):
|
||||
# Punycode is now legal, so no longer an error; instead check
|
||||
# that it's _not_ an error (at the initial sanity check stage)
|
||||
self._call('this.is.xn--ls8h.tld')
|
||||
|
||||
|
||||
class OsInfoTest(unittest.TestCase):
|
||||
"""Test OS / distribution detection"""
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import sys
|
|||
|
||||
import configargparse
|
||||
|
||||
from certbot import constants
|
||||
from certbot import errors
|
||||
|
||||
|
||||
|
|
@ -37,20 +38,22 @@ ANSI_SGR_RED = "\033[31m"
|
|||
ANSI_SGR_RESET = "\033[0m"
|
||||
|
||||
|
||||
def run_script(params):
|
||||
def run_script(params, log=logger.error):
|
||||
"""Run the script with the given params.
|
||||
|
||||
:param list params: List of parameters to pass to Popen
|
||||
:param logging.Logger log: Logger to use for errors
|
||||
|
||||
"""
|
||||
try:
|
||||
proc = subprocess.Popen(params,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE)
|
||||
stderr=subprocess.PIPE,
|
||||
universal_newlines=True)
|
||||
|
||||
except (OSError, ValueError):
|
||||
msg = "Unable to run the command: %s" % " ".join(params)
|
||||
logger.error(msg)
|
||||
log(msg)
|
||||
raise errors.SubprocessError(msg)
|
||||
|
||||
stdout, stderr = proc.communicate()
|
||||
|
|
@ -59,7 +62,7 @@ def run_script(params):
|
|||
msg = "Error while running %s.\n%s\n%s" % (
|
||||
" ".join(params), stdout, stderr)
|
||||
# Enter recovery routine...
|
||||
logger.error(msg)
|
||||
log(msg)
|
||||
raise errors.SubprocessError(msg)
|
||||
|
||||
return stdout, stderr
|
||||
|
|
@ -151,11 +154,11 @@ def safe_open(path, mode="w", chmod=None, buffering=None):
|
|||
mode, *fdopen_args)
|
||||
|
||||
|
||||
def _unique_file(path, filename_pat, count, mode):
|
||||
def _unique_file(path, filename_pat, count, chmod, mode):
|
||||
while True:
|
||||
current_path = os.path.join(path, filename_pat(count))
|
||||
try:
|
||||
return safe_open(current_path, chmod=mode),\
|
||||
return safe_open(current_path, chmod=chmod, mode=mode),\
|
||||
os.path.abspath(current_path)
|
||||
except OSError as err:
|
||||
# "File exists," is okay, try a different name.
|
||||
|
|
@ -164,11 +167,12 @@ def _unique_file(path, filename_pat, count, mode):
|
|||
count += 1
|
||||
|
||||
|
||||
def unique_file(path, mode=0o777):
|
||||
def unique_file(path, chmod=0o777, mode="w"):
|
||||
"""Safely finds a unique file.
|
||||
|
||||
:param str path: path/filename.ext
|
||||
:param int mode: File mode
|
||||
:param int chmod: File mode
|
||||
:param str mode: Open mode
|
||||
|
||||
:returns: tuple of file object and file name
|
||||
|
||||
|
|
@ -176,15 +180,16 @@ def unique_file(path, mode=0o777):
|
|||
path, tail = os.path.split(path)
|
||||
return _unique_file(
|
||||
path, filename_pat=(lambda count: "%04d_%s" % (count, tail)),
|
||||
count=0, mode=mode)
|
||||
count=0, chmod=chmod, mode=mode)
|
||||
|
||||
|
||||
def unique_lineage_name(path, filename, mode=0o777):
|
||||
def unique_lineage_name(path, filename, chmod=0o644, mode="w"):
|
||||
"""Safely finds a unique file using lineage convention.
|
||||
|
||||
:param str path: directory path
|
||||
:param str filename: proposed filename
|
||||
:param int mode: file mode
|
||||
:param int chmod: file mode
|
||||
:param str mode: open mode
|
||||
|
||||
:returns: tuple of file object and file name (which may be modified
|
||||
from the requested one by appending digits to ensure uniqueness)
|
||||
|
|
@ -196,13 +201,13 @@ def unique_lineage_name(path, filename, mode=0o777):
|
|||
"""
|
||||
preferred_path = os.path.join(path, "%s.conf" % (filename))
|
||||
try:
|
||||
return safe_open(preferred_path, chmod=mode), preferred_path
|
||||
return safe_open(preferred_path, chmod=chmod), preferred_path
|
||||
except OSError as err:
|
||||
if err.errno != errno.EEXIST:
|
||||
raise
|
||||
return _unique_file(
|
||||
path, filename_pat=(lambda count: "%s-%04d.conf" % (filename, count)),
|
||||
count=1, mode=mode)
|
||||
count=1, chmod=chmod, mode=mode)
|
||||
|
||||
|
||||
def safely_remove(path):
|
||||
|
|
@ -422,7 +427,6 @@ def enforce_le_validity(domain):
|
|||
label, domain))
|
||||
return domain
|
||||
|
||||
|
||||
def enforce_domain_sanity(domain):
|
||||
"""Method which validates domain value and errors out if
|
||||
the requirements are not met.
|
||||
|
|
@ -451,12 +455,8 @@ def enforce_domain_sanity(domain):
|
|||
domain = domain.decode('utf-8')
|
||||
domain.encode('ascii')
|
||||
except UnicodeError:
|
||||
error_fmt = (u"Internationalized domain names "
|
||||
"are not presently supported: {0}")
|
||||
if isinstance(domain, six.text_type):
|
||||
raise errors.ConfigurationError(error_fmt.format(domain))
|
||||
else:
|
||||
raise errors.ConfigurationError(str(error_fmt).format(domain))
|
||||
raise errors.ConfigurationError("Non-ASCII domain names not supported. "
|
||||
"To issue for an Internationalized Domain Name, use Punycode.")
|
||||
|
||||
domain = domain.lower()
|
||||
|
||||
|
|
@ -478,13 +478,15 @@ def enforce_domain_sanity(domain):
|
|||
# FQDN checks according to RFC 2181: domain name should be less than 255
|
||||
# octets (inclusive). And each label is 1 - 63 octets (inclusive).
|
||||
# https://tools.ietf.org/html/rfc2181#section-11
|
||||
msg = "Requested domain {0} is not a FQDN because ".format(domain)
|
||||
msg = "Requested domain {0} is not a FQDN because".format(domain)
|
||||
if len(domain) > 255:
|
||||
raise errors.ConfigurationError("{0} it is too long.".format(msg))
|
||||
labels = domain.split('.')
|
||||
for l in labels:
|
||||
if not 0 < len(l) < 64:
|
||||
raise errors.ConfigurationError(msg + "label {0} is too long.".format(l))
|
||||
if len(domain) > 255:
|
||||
raise errors.ConfigurationError(msg + "it is too long.")
|
||||
if not l:
|
||||
raise errors.ConfigurationError("{0} it contains an empty label.".format(msg))
|
||||
elif len(l) > 63:
|
||||
raise errors.ConfigurationError("{0} label {1} is too long.".format(msg, l))
|
||||
|
||||
return domain
|
||||
|
||||
|
|
@ -501,3 +503,14 @@ def get_strict_version(normalized):
|
|||
# strict version ending with "a" and a number designates a pre-release
|
||||
# pylint: disable=no-member
|
||||
return distutils.version.StrictVersion(normalized.replace(".dev", "a"))
|
||||
|
||||
|
||||
def is_staging(srv):
|
||||
"""
|
||||
Determine whether a given ACME server is a known test / staging server.
|
||||
|
||||
:param str srv: the URI for the ACME server
|
||||
:returns: True iff srv is a known test / staging server
|
||||
:rtype bool:
|
||||
"""
|
||||
return srv == constants.STAGING_URI or "staging" in srv
|
||||
|
|
|
|||
|
|
@ -1,5 +0,0 @@
|
|||
:mod:`certbot.log`
|
||||
----------------------
|
||||
|
||||
.. automodule:: certbot.log
|
||||
:members:
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue