Merge branch 'master-upstream' into forbid-readlink

This commit is contained in:
Adrien Ferrand 2021-01-04 21:24:00 +01:00
commit 5eec69236d
142 changed files with 1263 additions and 2100 deletions

View file

@ -79,8 +79,6 @@ jobs:
IMAGE_NAME: ubuntu-18.04
PYTHON_VERSION: 3.8
TOXENV: integration-dns-rfc2136
le-auto-oraclelinux6:
TOXENV: le_auto_oraclelinux6
docker-dev:
TOXENV: docker_dev
macos-farmtest-apache2:

View file

@ -144,7 +144,7 @@ jobs:
git config --global user.name "$(Build.RequestedFor)"
mkdir -p ~/.local/share/snapcraft/provider/launchpad
cp $(credentials.secureFilePath) ~/.local/share/snapcraft/provider/launchpad/credentials
python3 tools/snap/build_remote.py ALL --archs ${ARCHS}
python3 tools/snap/build_remote.py ALL --archs ${ARCHS} --timeout 19800
displayName: Build snaps
- script: |
set -e

View file

@ -58,9 +58,9 @@ jobs:
apache-compat:
IMAGE_NAME: ubuntu-18.04
TOXENV: apache_compat
le-auto-centos6:
le-modification:
IMAGE_NAME: ubuntu-18.04
TOXENV: le_auto_centos6
TOXENV: modification
apacheconftest:
IMAGE_NAME: ubuntu-18.04
PYTHON_VERSION: 2.7

View file

@ -149,6 +149,7 @@ Authors
* [Lior Sabag](https://github.com/liorsbg)
* [Lipis](https://github.com/lipis)
* [lord63](https://github.com/lord63)
* [Lorenzo Fundaró](https://github.com/lfundaro)
* [Luca Beltrame](https://github.com/lbeltrame)
* [Luca Ebach](https://github.com/lucebac)
* [Luca Olivetti](https://github.com/olivluca)

View file

@ -20,3 +20,10 @@ for mod in list(sys.modules):
# preserved (acme.jose.* is josepy.*)
if mod == 'josepy' or mod.startswith('josepy.'):
sys.modules['acme.' + mod.replace('josepy', 'jose', 1)] = sys.modules[mod]
if sys.version_info[0] == 2:
warnings.warn(
"Python 2 support will be dropped in the next release of acme. "
"Please upgrade your Python version.",
PendingDeprecationWarning,
) # pragma: no cover

View file

@ -186,6 +186,7 @@ def probe_sni(name, host, port=443, timeout=300, # pylint: disable=too-many-argu
raise errors.Error(error)
return client_ssl.get_peer_certificate()
def make_csr(private_key_pem, domains, must_staple=False):
"""Generate a CSR containing a list of domains as subjectAltNames.
@ -217,6 +218,7 @@ def make_csr(private_key_pem, domains, must_staple=False):
return crypto.dump_certificate_request(
crypto.FILETYPE_PEM, csr)
def _pyopenssl_cert_or_req_all_names(loaded_cert_or_req):
common_name = loaded_cert_or_req.get_subject().CN
sans = _pyopenssl_cert_or_req_san(loaded_cert_or_req)
@ -225,6 +227,7 @@ def _pyopenssl_cert_or_req_all_names(loaded_cert_or_req):
return sans
return [common_name] + [d for d in sans if d != common_name]
def _pyopenssl_cert_or_req_san(cert_or_req):
"""Get Subject Alternative Names from certificate or CSR using pyOpenSSL.
@ -317,6 +320,7 @@ def gen_ss_cert(key, domains, not_before=None,
cert.sign(key, "sha256")
return cert
def dump_pyopenssl_chain(chain, filetype=crypto.FILETYPE_PEM):
"""Dump certificate chain into a bundle.

View file

@ -5,7 +5,7 @@ from setuptools import __version__ as setuptools_version
from setuptools import find_packages
from setuptools import setup
version = '1.10.0.dev0'
version = '1.11.0.dev0'
# Please update tox.ini when modifying dependency version requirements
install_requires = [

View file

@ -327,6 +327,9 @@ class ApacheConfigurator(common.Installer):
if self.version < (2, 2):
raise errors.NotSupportedError(
"Apache Version {0} not supported.".format(str(self.version)))
elif self.version < (2, 4):
logger.warning('Support for Apache 2.2 is deprecated and will be removed in a '
'future release.')
# Recover from previous crash before Augeas initialization to have the
# correct parse tree from the get go.

View file

@ -5,7 +5,7 @@ from setuptools import __version__ as setuptools_version
from setuptools import find_packages
from setuptools import setup
version = '1.10.0.dev0'
version = '1.11.0.dev0'
# Remember to update local-oldest-requirements.txt when changing the minimum
# acme/certbot version.

View file

@ -31,7 +31,7 @@ if [ -z "$VENV_PATH" ]; then
fi
VENV_BIN="$VENV_PATH/bin"
BOOTSTRAP_VERSION_PATH="$VENV_PATH/certbot-auto-bootstrap-version.txt"
LE_AUTO_VERSION="1.9.0"
LE_AUTO_VERSION="1.10.1"
BASENAME=$(basename $0)
USAGE="Usage: $BASENAME [OPTIONS]
A self-updating wrapper script for the Certbot ACME client. When run, updates
@ -799,11 +799,7 @@ BootstrapMageiaCommon() {
# that function. If Bootstrap is set to a function that doesn't install any
# packages BOOTSTRAP_VERSION is not set.
if [ -f /etc/debian_version ]; then
Bootstrap() {
BootstrapMessage "Debian-based OSes"
BootstrapDebCommon
}
BOOTSTRAP_VERSION="BootstrapDebCommon $BOOTSTRAP_DEB_COMMON_VERSION"
DEPRECATED_OS=1
elif [ -f /etc/mageia-release ]; then
# Mageia has both /etc/mageia-release and /etc/redhat-release
DEPRECATED_OS=1
@ -1497,18 +1493,18 @@ letsencrypt==0.7.0 \
--hash=sha256:105a5fb107e45bcd0722eb89696986dcf5f08a86a321d6aef25a0c7c63375ade \
--hash=sha256:c36e532c486a7e92155ee09da54b436a3c420813ec1c590b98f635d924720de9
certbot==1.9.0 \
--hash=sha256:d5a804d32e471050921f7b39ed9859e2e9de02824176ed78f57266222036b53a \
--hash=sha256:2ff9bf7d9af381c7efee22dec2dd6938d9d8fddcc9e11682b86e734164a30b57
acme==1.9.0 \
--hash=sha256:d8061b396a22b21782c9b23ff9a945b23e50fca2573909a42f845e11d5658ac5 \
--hash=sha256:38a1630c98e144136c62eec4d2c545a1bdb1a3cd4eca82214be6b83a1f5a161f
certbot-apache==1.9.0 \
--hash=sha256:09528a820d57e54984d490100644cd8a6603db97bf5776f86e95795ecfacf23d \
--hash=sha256:f47fb3f4a9bd927f4812121a0beefe56b163475a28f4db34c64dc838688d9e9e
certbot-nginx==1.9.0 \
--hash=sha256:bb2e3f7fe17f071f350a3efa48571b8ef40a8e4b6db9c6da72539206a20b70be \
--hash=sha256:ab26a4f49d53b0e8bf0f903e58e2a840cda233fe1cbbc54c36ff17f973e57d65
certbot==1.10.1 \
--hash=sha256:011ac980fa21b9f29e02c9b8d8b86e8a4bf4670b51b6ad91656e401e9d2d2231 \
--hash=sha256:0d9ee3fc09e0d03b2d1b1f1c4916e61ecfc6904b4216ddef4e6a5ca1424d9cb7
acme==1.10.1 \
--hash=sha256:752d598e54e98ad1e874de53fd50c61044f1b566d6deb790db5676ce9c573546 \
--hash=sha256:fcbb559aedc96b404edf593e78517dcd7291984d5a37036c3fc77f3c5c122fd8
certbot-apache==1.10.1 \
--hash=sha256:f077b4b7f166627ef5e0921fe7cde57700670fc86e9ad9dbdfaf2c573cc0f2fa \
--hash=sha256:97ed637b4c7b03820db6c69aa90145dc989933351d46a3d62baf6b71674f0a10
certbot-nginx==1.10.1 \
--hash=sha256:7c36459021f8a1ec3b6c062e4c4fc866bfaa1dbf26ccd29e043dd6848003be08 \
--hash=sha256:c0bbeccf85f46b728fd95e6bb8c2649d32d3383d7f47ea4b9c312d12bf04d2f0
UNLIKELY_EOF
# -------------------------------------------------------------------------

View file

@ -1,3 +1,4 @@
# pylint: disable=missing-module-docstring
import pytest
# Custom assertions defined in the following package need to be registered to be properly

View file

@ -77,6 +77,6 @@ class IntegrationTestsContext(object):
appending the pytest worker id to the subdomain, using this pattern:
{subdomain}.{worker_id}.wtf
:param subdomain: the subdomain to use in the generated domain (default 'le')
:return: the well-formed domain suitable for redirection on
:return: the well-formed domain suitable for redirection on
"""
return '{0}.{1}.wtf'.format(subdomain, self.worker_id)

View file

@ -29,8 +29,9 @@ from certbot_integration_tests.certbot_tests.assertions import EVERYBODY_SID
from certbot_integration_tests.utils import misc
@pytest.fixture()
def context(request):
@pytest.fixture(name='context')
def test_context(request):
# pylint: disable=missing-function-docstring
# Fixture request is a built-in pytest fixture describing current test request.
integration_test_context = certbot_context.IntegrationTestsContext(request)
try:
@ -222,14 +223,16 @@ def test_renew_files_propagate_permissions(context):
if os.name != 'nt':
os.chmod(privkey1, 0o444)
else:
import win32security
import ntsecuritycon
import win32security # pylint: disable=import-error
import ntsecuritycon # pylint: disable=import-error
# Get the current DACL of the private key
security = win32security.GetFileSecurity(privkey1, win32security.DACL_SECURITY_INFORMATION)
dacl = security.GetSecurityDescriptorDacl()
# Create a read permission for Everybody group
everybody = win32security.ConvertStringSidToSid(EVERYBODY_SID)
dacl.AddAccessAllowedAce(win32security.ACL_REVISION, ntsecuritycon.FILE_GENERIC_READ, everybody)
dacl.AddAccessAllowedAce(
win32security.ACL_REVISION, ntsecuritycon.FILE_GENERIC_READ, everybody
)
# Apply the updated DACL to the private key
security.SetSecurityDescriptorDacl(1, dacl, 0)
win32security.SetFileSecurity(privkey1, win32security.DACL_SECURITY_INFORMATION, security)
@ -238,12 +241,14 @@ def test_renew_files_propagate_permissions(context):
assert_cert_count_for_lineage(context.config_dir, certname, 2)
if os.name != 'nt':
# On Linux, read world permissions + all group permissions will be copied from the previous private key
# On Linux, read world permissions + all group permissions
# will be copied from the previous private key
assert_world_read_permissions(privkey2)
assert_equals_world_read_permissions(privkey1, privkey2)
assert_equals_group_permissions(privkey1, privkey2)
else:
# On Windows, world will never have any permissions, and group permission is irrelevant for this platform
# On Windows, world will never have any permissions, and
# group permission is irrelevant for this platform
assert_world_no_permissions(privkey2)
@ -609,19 +614,22 @@ def test_revoke_multiple_lineages(context):
with open(join(context.config_dir, 'renewal', '{0}.conf'.format(cert2)), 'r') as file:
data = file.read()
data = re.sub('archive_dir = .*\n',
'archive_dir = {0}\n'.format(join(context.config_dir, 'archive', cert1).replace('\\', '\\\\')),
data)
data = re.sub(
'archive_dir = .*\n',
'archive_dir = {0}\n'.format(
join(context.config_dir, 'archive', cert1).replace('\\', '\\\\')
), data
)
with open(join(context.config_dir, 'renewal', '{0}.conf'.format(cert2)), 'w') as file:
file.write(data)
output = context.certbot([
context.certbot([
'revoke', '--cert-path', join(context.config_dir, 'live', cert1, 'cert.pem')
])
with open(join(context.workspace, 'logs', 'letsencrypt.log'), 'r') as f:
assert 'Not deleting revoked certs due to overlapping archive dirs' in f.read()
assert 'Not deleting revoked certificates due to overlapping archive dirs' in f.read()
def test_wildcard_certificates(context):

View file

@ -13,7 +13,6 @@ import sys
from certbot_integration_tests.utils import acme_server as acme_lib
from certbot_integration_tests.utils import dns_server as dns_lib
from certbot_integration_tests.utils.dns_server import DNSServer
def pytest_addoption(parser):
@ -92,8 +91,10 @@ def _setup_primary_node(config):
try:
subprocess.check_output(['docker-compose', '-v'], stderr=subprocess.STDOUT)
except (subprocess.CalledProcessError, OSError):
raise ValueError('Error: docker-compose is required in PATH to launch the integration tests, '
'but is not installed or not available for current user.')
raise ValueError(
'Error: docker-compose is required in PATH to launch the integration tests, '
'but is not installed or not available for current user.'
)
# Parameter numprocesses is added to option by pytest-xdist
workers = ['primary'] if not config.option.numprocesses\

View file

@ -1,3 +1,4 @@
"""Module to handle the context of nginx integration tests."""
import os
import subprocess

View file

@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
"""General purpose nginx test configuration generator."""
import getpass
@ -42,6 +43,8 @@ events {{
worker_connections 1024;
}}
# “This comment contains valid Unicode”.
http {{
# Set an array of temp, cache and log file options that will otherwise default to
# restricted locations accessible only to root.
@ -51,61 +54,61 @@ http {{
#scgi_temp_path {nginx_root}/scgi_temp;
#uwsgi_temp_path {nginx_root}/uwsgi_temp;
access_log {nginx_root}/error.log;
# This should be turned off in a Virtualbox VM, as it can cause some
# interesting issues with data corruption in delivered files.
sendfile off;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
#include /etc/nginx/mime.types;
index index.html index.htm index.php;
log_format main '$remote_addr - $remote_user [$time_local] $status '
'"$request" $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
default_type application/octet-stream;
server {{
# IPv4.
listen {http_port} {default_server};
# IPv6.
listen [::]:{http_port} {default_server};
server_name nginx.{wtf_prefix}.wtf nginx2.{wtf_prefix}.wtf;
root {nginx_webroot};
location / {{
# First attempt to serve request as file, then as directory, then fall
# back to index.html.
try_files $uri $uri/ /index.html;
}}
}}
server {{
listen {http_port};
listen [::]:{http_port};
server_name nginx3.{wtf_prefix}.wtf;
root {nginx_webroot};
location /.well-known/ {{
return 404;
}}
return 301 https://$host$request_uri;
}}
server {{
listen {other_port};
listen [::]:{other_port};
server_name nginx4.{wtf_prefix}.wtf nginx5.{wtf_prefix}.wtf;
}}
server {{
listen {http_port};
listen [::]:{http_port};

View file

@ -2,13 +2,14 @@
import os
import ssl
from typing import List
import pytest
from certbot_integration_tests.nginx_tests import context as nginx_context
@pytest.fixture()
def context(request):
@pytest.fixture(name='context')
def test_context(request):
# Fixture request is a built-in pytest fixture describing current test request.
integration_test_context = nginx_context.IntegrationTestsContext(request)
try:
@ -27,10 +28,12 @@ def context(request):
# No matching server block; default_server does not exist
('nginx5.{0}.wtf', ['--preferred-challenges', 'http'], {'default_server': False}),
# Multiple domains, mix of matching and not
('nginx6.{0}.wtf,nginx7.{0}.wtf', ['--preferred-challenges', 'http'], {'default_server': False}),
('nginx6.{0}.wtf,nginx7.{0}.wtf', [
'--preferred-challenges', 'http'
], {'default_server': False}),
], indirect=['context'])
def test_certificate_deployment(certname_pattern, params, context):
# type: (str, list, nginx_context.IntegrationTestsContext) -> None
# type: (str, List[str], nginx_context.IntegrationTestsContext) -> None
"""
Test various scenarios to deploy a certificate to nginx using certbot.
"""
@ -41,7 +44,9 @@ def test_certificate_deployment(certname_pattern, params, context):
lineage = domains.split(',')[0]
server_cert = ssl.get_server_certificate(('localhost', context.tls_alpn_01_port))
with open(os.path.join(context.workspace, 'conf/live/{0}/cert.pem'.format(lineage)), 'r') as file:
with open(os.path.join(
context.workspace, 'conf/live/{0}/cert.pem'.format(lineage)), 'r'
) as file:
certbot_cert = file.read()
assert server_cert == certbot_cert

View file

@ -1,7 +1,10 @@
from contextlib import contextmanager
from pytest import skip
from pkg_resources import resource_filename
"""Module to handle the context of RFC2136 integration tests."""
import tempfile
from contextlib import contextmanager
from pkg_resources import resource_filename
from pytest import skip
from certbot_integration_tests.certbot_tests import context as certbot_context
from certbot_integration_tests.utils import certbot_call
@ -33,7 +36,6 @@ class IntegrationTestsContext(certbot_context.IntegrationTestsContext):
@contextmanager
def rfc2136_credentials(self, label='default'):
# type: (str) -> str
"""
Produces the contents of a certbot-dns-rfc2136 credentials file.
:param str label: which RFC2136 credential to use
@ -52,10 +54,10 @@ class IntegrationTestsContext(certbot_context.IntegrationTestsContext):
)
with tempfile.NamedTemporaryFile('w+', prefix='rfc2136-creds-{}'.format(label),
suffix='.ini', dir=self.workspace) as f:
f.write(contents)
f.flush()
yield f.name
suffix='.ini', dir=self.workspace) as fp:
fp.write(contents)
fp.flush()
yield fp.name
def skip_if_no_bind9_server(self):
"""Skips the test if there was no RFC2136-capable DNS server configured

View file

@ -4,8 +4,9 @@ import pytest
from certbot_integration_tests.rfc2136_tests import context as rfc2136_context
@pytest.fixture()
def context(request):
@pytest.fixture(name="context")
def pytest_context(request):
# pylint: disable=missing-function-docstring
# Fixture request is a built-in pytest fixture describing current test request.
integration_test_context = rfc2136_context.IntegrationTestsContext(request)
try:

View file

@ -7,18 +7,19 @@ import errno
import json
import os
from os.path import join
import re
import shutil
import subprocess
import sys
import tempfile
import time
from typing import List
import requests
from certbot_integration_tests.utils import misc
from certbot_integration_tests.utils import pebble_artifacts
from certbot_integration_tests.utils import proxy
# pylint: disable=wildcard-import,unused-wildcard-import
from certbot_integration_tests.utils.constants import *
@ -31,10 +32,11 @@ class ACMEServer(object):
ACMEServer gives access the acme_xdist parameter, listing the ports and directory url to use
for each pytest node. It exposes also start and stop methods in order to start the stack, and
stop it with proper resources cleanup.
ACMEServer is also a context manager, and so can be used to ensure ACME server is started/stopped
upon context enter/exit.
ACMEServer is also a context manager, and so can be used to ensure ACME server is
started/stopped upon context enter/exit.
"""
def __init__(self, acme_server, nodes, http_proxy=True, stdout=False, dns_server=None):
def __init__(self, acme_server, nodes, http_proxy=True, stdout=False,
dns_server=None, http_01_port=DEFAULT_HTTP_01_PORT):
"""
Create an ACMEServer instance.
:param str acme_server: the type of acme server used (boulder-v1, boulder-v2 or pebble)
@ -42,15 +44,22 @@ class ACMEServer(object):
:param bool http_proxy: if False do not start the HTTP proxy
:param bool stdout: if True stream all subprocesses stdout to standard stdout
:param str dns_server: if set, Pebble/Boulder will use it to resolve domains
:param int http_01_port: port to use for http-01 validation; currently
only supported for pebble without an HTTP proxy
"""
self._construct_acme_xdist(acme_server, nodes)
self._acme_type = 'pebble' if acme_server == 'pebble' else 'boulder'
self._proxy = http_proxy
self._workspace = tempfile.mkdtemp()
self._processes = []
self._processes = [] # type: List[subprocess.Popen]
self._stdout = sys.stdout if stdout else open(os.devnull, 'w')
self._dns_server = dns_server
self._http_01_port = http_01_port
if http_01_port != DEFAULT_HTTP_01_PORT:
if self._acme_type != 'pebble' or self._proxy:
raise ValueError('setting http_01_port is not currently supported '
'with boulder or the HTTP proxy')
def start(self):
"""Start the test stack"""
@ -107,26 +116,34 @@ class ACMEServer(object):
"""Generate and return the acme_xdist dict"""
acme_xdist = {'acme_server': acme_server, 'challtestsrv_port': CHALLTESTSRV_PORT}
# Directory and ACME port are set implicitly in the docker-compose.yml files of Boulder/Pebble.
# Directory and ACME port are set implicitly in the docker-compose.yml
# files of Boulder/Pebble.
if acme_server == 'pebble':
acme_xdist['directory_url'] = PEBBLE_DIRECTORY_URL
else: # boulder
acme_xdist['directory_url'] = BOULDER_V2_DIRECTORY_URL \
if acme_server == 'boulder-v2' else BOULDER_V1_DIRECTORY_URL
acme_xdist['http_port'] = {node: port for (node, port)
in zip(nodes, range(5200, 5200 + len(nodes)))}
acme_xdist['https_port'] = {node: port for (node, port)
in zip(nodes, range(5100, 5100 + len(nodes)))}
acme_xdist['other_port'] = {node: port for (node, port)
in zip(nodes, range(5300, 5300 + len(nodes)))}
acme_xdist['http_port'] = {
node: port for (node, port) in # pylint: disable=unnecessary-comprehension
zip(nodes, range(5200, 5200 + len(nodes)))
}
acme_xdist['https_port'] = {
node: port for (node, port) in # pylint: disable=unnecessary-comprehension
zip(nodes, range(5100, 5100 + len(nodes)))
}
acme_xdist['other_port'] = {
node: port for (node, port) in # pylint: disable=unnecessary-comprehension
zip(nodes, range(5300, 5300 + len(nodes)))
}
self.acme_xdist = acme_xdist
def _prepare_pebble_server(self):
"""Configure and launch the Pebble server"""
print('=> Starting pebble instance deployment...')
pebble_path, challtestsrv_path, pebble_config_path = pebble_artifacts.fetch(self._workspace)
pebble_artifacts_rv = pebble_artifacts.fetch(self._workspace, self._http_01_port)
pebble_path, challtestsrv_path, pebble_config_path = pebble_artifacts_rv
# Configure Pebble at full speed (PEBBLE_VA_NOSLEEP=1) and not randomly refusing valid
# nonce (PEBBLE_WFE_NONCEREJECT=0) to have a stable test environment.
@ -149,10 +166,10 @@ class ACMEServer(object):
[pebble_path, '-config', pebble_config_path, '-dnsserver', dns_server, '-strict'],
env=environ)
# pebble_ocsp_server is imported here and not at the top of module in order to avoid a useless
# ImportError, in the case where cryptography dependency is too old to support ocsp, but
# Boulder is used instead of Pebble, so pebble_ocsp_server is not used. This is the typical
# situation of integration-certbot-oldest tox testenv.
# pebble_ocsp_server is imported here and not at the top of module in order to avoid a
# useless ImportError, in the case where cryptography dependency is too old to support
# ocsp, but Boulder is used instead of Pebble, so pebble_ocsp_server is not used. This is
# the typical situation of integration-certbot-oldest tox testenv.
from certbot_integration_tests.utils import pebble_ocsp_server
self._launch_process([sys.executable, pebble_ocsp_server.__file__])
@ -178,11 +195,12 @@ class ACMEServer(object):
if self._dns_server:
# Change Boulder config to use the provided DNS server
with open(join(instance_path, 'test/config/va.json'), 'r') as file_h:
config = json.loads(file_h.read())
config['va']['dnsResolvers'] = [self._dns_server]
with open(join(instance_path, 'test/config/va.json'), 'w') as file_h:
file_h.write(json.dumps(config, indent=2, separators=(',', ': ')))
for suffix in ["", "-remote-a", "-remote-b"]:
with open(join(instance_path, 'test/config/va{}.json'.format(suffix)), 'r') as f:
config = json.loads(f.read())
config['va']['dnsResolvers'] = [self._dns_server]
with open(join(instance_path, 'test/config/va{}.json'.format(suffix)), 'w') as f:
f.write(json.dumps(config, indent=2, separators=(',', ': ')))
try:
# Launch the Boulder server
@ -194,13 +212,16 @@ class ACMEServer(object):
if not self._dns_server:
# Configure challtestsrv to answer any A record request with ip of the docker host.
response = requests.post('http://localhost:{0}/set-default-ipv4'.format(CHALLTESTSRV_PORT),
json={'ip': '10.77.77.1'})
response = requests.post('http://localhost:{0}/set-default-ipv4'.format(
CHALLTESTSRV_PORT), json={'ip': '10.77.77.1'}
)
response.raise_for_status()
except BaseException:
# If we failed to set up boulder, print its logs.
print('=> Boulder setup failed. Boulder logs are:')
process = self._launch_process(['docker-compose', 'logs'], cwd=instance_path, force_stderr=True)
process = self._launch_process([
'docker-compose', 'logs'], cwd=instance_path, force_stderr=True
)
process.wait()
raise
@ -211,7 +232,7 @@ class ACMEServer(object):
print('=> Configuring the HTTP proxy...')
mapping = {r'.+\.{0}\.wtf'.format(node): 'http://127.0.0.1:{0}'.format(port)
for node, port in self.acme_xdist['http_port'].items()}
command = [sys.executable, proxy.__file__, str(HTTP_01_PORT), json.dumps(mapping)]
command = [sys.executable, proxy.__file__, str(DEFAULT_HTTP_01_PORT), json.dumps(mapping)]
self._launch_process(command)
print('=> Finished configuring the HTTP proxy.')
@ -220,12 +241,15 @@ class ACMEServer(object):
if not env:
env = os.environ
stdout = sys.stderr if force_stderr else self._stdout
process = subprocess.Popen(command, stdout=stdout, stderr=subprocess.STDOUT, cwd=cwd, env=env)
process = subprocess.Popen(
command, stdout=stdout, stderr=subprocess.STDOUT, cwd=cwd, env=env
)
self._processes.append(process)
return process
def main():
# pylint: disable=missing-function-docstring
parser = argparse.ArgumentParser(
description='CLI tool to start a local instance of Pebble or Boulder CA server.')
parser.add_argument('--server-type', '-s',
@ -236,9 +260,15 @@ def main():
help='specify the DNS server as `IP:PORT` to use by '
'Pebble; if not specified, a local mock DNS server will be used to '
'resolve domains to localhost.')
parser.add_argument('--http-01-port', type=int, default=DEFAULT_HTTP_01_PORT,
help='specify the port to use for http-01 validation; '
'this is currently only supported for Pebble.')
args = parser.parse_args()
acme_server = ACMEServer(args.server_type, [], http_proxy=False, stdout=True, dns_server=args.dns_server)
acme_server = ACMEServer(
args.server_type, [], http_proxy=False, stdout=True,
dns_server=args.dns_server, http_01_port=args.http_01_port,
)
try:
with acme_server as acme_xdist:

View file

@ -2,12 +2,13 @@
"""Module to call certbot in test mode"""
from __future__ import absolute_import
from distutils.version import LooseVersion
import os
import subprocess
import sys
from distutils.version import LooseVersion
import certbot_integration_tests
# pylint: disable=wildcard-import,unused-wildcard-import
from certbot_integration_tests.utils.constants import *
@ -35,6 +36,8 @@ def certbot_test(certbot_args, directory_url, http_01_port, tls_alpn_01_port,
def _prepare_environ(workspace):
# pylint: disable=missing-function-docstring
new_environ = os.environ.copy()
new_environ['TMPDIR'] = workspace
@ -58,8 +61,13 @@ def _prepare_environ(workspace):
# certbot_integration_tests.__file__ is:
# '/path/to/certbot/certbot-ci/certbot_integration_tests/__init__.pyc'
# ... and we want '/path/to/certbot'
certbot_root = os.path.dirname(os.path.dirname(os.path.dirname(certbot_integration_tests.__file__)))
python_paths = [path for path in new_environ['PYTHONPATH'].split(':') if path != certbot_root]
certbot_root = os.path.dirname(os.path.dirname(
os.path.dirname(certbot_integration_tests.__file__))
)
python_paths = [
path for path in new_environ['PYTHONPATH'].split(':')
if path != certbot_root
]
new_environ['PYTHONPATH'] = ':'.join(python_paths)
return new_environ
@ -70,7 +78,8 @@ def _compute_additional_args(workspace, environ, force_renew):
output = subprocess.check_output(['certbot', '--version'],
universal_newlines=True, stderr=subprocess.STDOUT,
cwd=workspace, env=environ)
version_str = output.split(' ')[1].strip() # Typical response is: output = 'certbot 0.31.0.dev0'
# Typical response is: output = 'certbot 0.31.0.dev0'
version_str = output.split(' ')[1].strip()
if LooseVersion(version_str) >= LooseVersion('0.30.0'):
additional_args.append('--no-random-sleep-on-renew')
@ -92,6 +101,7 @@ def _prepare_args_env(certbot_args, directory_url, http_01_port, tls_alpn_01_por
'--no-verify-ssl',
'--http-01-port', str(http_01_port),
'--https-port', str(tls_alpn_01_port),
'--manual-public-ip-logging-ok',
'--config-dir', config_dir,
'--work-dir', os.path.join(workspace, 'work'),
'--logs-dir', os.path.join(workspace, 'logs'),
@ -112,11 +122,12 @@ def _prepare_args_env(certbot_args, directory_url, http_01_port, tls_alpn_01_por
def main():
# pylint: disable=missing-function-docstring
args = sys.argv[1:]
# Default config is pebble
directory_url = os.environ.get('SERVER', PEBBLE_DIRECTORY_URL)
http_01_port = int(os.environ.get('HTTP_01_PORT', HTTP_01_PORT))
http_01_port = int(os.environ.get('HTTP_01_PORT', DEFAULT_HTTP_01_PORT))
tls_alpn_01_port = int(os.environ.get('TLS_ALPN_01_PORT', TLS_ALPN_01_PORT))
# Execution of certbot in a self-contained workspace

View file

@ -1,5 +1,5 @@
"""Some useful constants to use throughout certbot-ci integration tests"""
HTTP_01_PORT = 5002
DEFAULT_HTTP_01_PORT = 5002
TLS_ALPN_01_PORT = 5001
CHALLTESTSRV_PORT = 8055
BOULDER_V1_DIRECTORY_URL = 'http://localhost:4000/directory'
@ -7,4 +7,4 @@ BOULDER_V2_DIRECTORY_URL = 'http://localhost:4001/directory'
PEBBLE_DIRECTORY_URL = 'https://localhost:14000/dir'
PEBBLE_MANAGEMENT_URL = 'https://localhost:15000'
MOCK_OCSP_SERVER_PORT = 4002
PEBBLE_ALTERNATE_ROOTS = 2
PEBBLE_ALTERNATE_ROOTS = 2

View file

@ -4,7 +4,6 @@ from __future__ import print_function
import os
import os.path
from pkg_resources import resource_filename
import shutil
import socket
import subprocess
@ -12,13 +11,14 @@ import sys
import tempfile
import time
from pkg_resources import resource_filename
BIND_DOCKER_IMAGE = 'internetsystemsconsortium/bind9:9.16'
BIND_BIND_ADDRESS = ('127.0.0.1', 45953)
BIND_DOCKER_IMAGE = "internetsystemsconsortium/bind9:9.16"
BIND_BIND_ADDRESS = ("127.0.0.1", 45953)
# A TCP DNS message which is a query for '. CH A' transaction ID 0xcb37. This is used
# by _wait_until_ready to check that BIND is responding without depending on dnspython.
BIND_TEST_QUERY = bytearray.fromhex('0011cb37000000010000000000000000010003')
BIND_TEST_QUERY = bytearray.fromhex("0011cb37000000010000000000000000010003")
class DNSServer(object):
@ -31,7 +31,7 @@ class DNSServer(object):
future to support parallelization (https://github.com/certbot/certbot/issues/8455).
"""
def __init__(self, nodes, show_output=False):
def __init__(self, unused_nodes, show_output=False):
"""
Create an DNSServer instance.
:param list nodes: list of node names that will be setup by pytest xdist
@ -40,16 +40,13 @@ class DNSServer(object):
self.bind_root = tempfile.mkdtemp()
self.process = None
self.process = None # type: subprocess.Popen
self.dns_xdist = {
'address': BIND_BIND_ADDRESS[0],
'port': BIND_BIND_ADDRESS[1]
}
self.dns_xdist = {"address": BIND_BIND_ADDRESS[0], "port": BIND_BIND_ADDRESS[1]}
# Unfortunately the BIND9 image forces everything to stderr with -g and we can't
# modify the verbosity.
self._output = sys.stderr if show_output else open(os.devnull, 'w')
self._output = sys.stderr if show_output else open(os.devnull, "w")
def start(self):
"""Start the DNS server"""
@ -63,11 +60,11 @@ class DNSServer(object):
def stop(self):
"""Stop the DNS server, and clean its resources"""
if self.process:
try:
self.process.terminate()
self.process.wait()
except BaseException as e:
print("BIND9 did not stop cleanly: {}".format(e), file=sys.stderr)
try:
self.process.terminate()
self.process.wait()
except BaseException as e:
print("BIND9 did not stop cleanly: {}".format(e), file=sys.stderr)
shutil.rmtree(self.bind_root, ignore_errors=True)
@ -76,65 +73,79 @@ class DNSServer(object):
def _configure_bind(self):
"""Configure the BIND9 server based on the prebaked configuration"""
bind_conf_src = resource_filename('certbot_integration_tests', 'assets/bind-config')
for dir in ('conf', 'zones'):
shutil.copytree(os.path.join(bind_conf_src, dir), os.path.join(self.bind_root, dir))
bind_conf_src = resource_filename(
"certbot_integration_tests", "assets/bind-config"
)
for directory in ("conf", "zones"):
shutil.copytree(
os.path.join(bind_conf_src, directory), os.path.join(self.bind_root, directory)
)
def _start_bind(self):
"""Launch the BIND9 server as a Docker container"""
addr_str = '{}:{}'.format(BIND_BIND_ADDRESS[0], BIND_BIND_ADDRESS[1])
self.process = subprocess.Popen([
'docker', 'run', '--rm',
'-p', '{}:53/udp'.format(addr_str),
'-p', '{}:53/tcp'.format(addr_str),
'-v', '{}/conf:/etc/bind'.format(self.bind_root),
'-v', '{}/zones:/var/lib/bind'.format(self.bind_root),
BIND_DOCKER_IMAGE
], stdout=self._output, stderr=self._output)
addr_str = "{}:{}".format(BIND_BIND_ADDRESS[0], BIND_BIND_ADDRESS[1])
self.process = subprocess.Popen(
[
"docker",
"run",
"--rm",
"-p",
"{}:53/udp".format(addr_str),
"-p",
"{}:53/tcp".format(addr_str),
"-v",
"{}/conf:/etc/bind".format(self.bind_root),
"-v",
"{}/zones:/var/lib/bind".format(self.bind_root),
BIND_DOCKER_IMAGE,
],
stdout=self._output,
stderr=self._output,
)
if self.process.poll():
raise("BIND9 server stopped unexpectedly")
raise ValueError("BIND9 server stopped unexpectedly")
try:
self._wait_until_ready()
self._wait_until_ready()
except:
# The container might be running even if we think it isn't
self.stop()
raise
# The container might be running even if we think it isn't
self.stop()
raise
def _wait_until_ready(self, attempts=30):
# type: (int) -> None
"""
Polls the DNS server over TCP until it gets a response, or until
it runs out of attempts and raises a ValueError.
The DNS response message must match the txn_id of the DNS query message,
but otherwise the contents are ignored.
:param int attempts: The number of attempts to make.
"""
for _ in range(attempts):
if self.process.poll():
raise ValueError('BIND9 server stopped unexpectedly')
# type: (int) -> None
"""
Polls the DNS server over TCP until it gets a response, or until
it runs out of attempts and raises a ValueError.
The DNS response message must match the txn_id of the DNS query message,
but otherwise the contents are ignored.
:param int attempts: The number of attempts to make.
"""
for _ in range(attempts):
if self.process.poll():
raise ValueError("BIND9 server stopped unexpectedly")
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(5.0)
try:
sock.connect(BIND_BIND_ADDRESS)
sock.sendall(BIND_TEST_QUERY)
buf = sock.recv(1024)
# We should receive a DNS message with the same tx_id
if buf and len(buf) > 4 and buf[2:4] == BIND_TEST_QUERY[2:4]:
return
# If we got a response but it wasn't the one we wanted, wait a little
time.sleep(1)
except:
# If there was a network error, wait a little
time.sleep(1)
pass
finally:
sock.close()
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(5.0)
try:
sock.connect(BIND_BIND_ADDRESS)
sock.sendall(BIND_TEST_QUERY)
buf = sock.recv(1024)
# We should receive a DNS message with the same tx_id
if buf and len(buf) > 4 and buf[2:4] == BIND_TEST_QUERY[2:4]:
return
# If we got a response but it wasn't the one we wanted, wait a little
time.sleep(1)
except: # pylint: disable=bare-except
# If there was a network error, wait a little
time.sleep(1)
finally:
sock.close()
raise ValueError(
'Gave up waiting for DNS server {} to respond'.format(BIND_BIND_ADDRESS))
raise ValueError(
"Gave up waiting for DNS server {} to respond".format(BIND_BIND_ADDRESS)
)
def __enter__(self):
self.start()

View file

@ -39,6 +39,7 @@ def _suppress_x509_verification_warnings():
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
except ImportError:
# Handle old versions of request with vendorized urllib3
# pylint: disable=no-member
from requests.packages.urllib3.exceptions import InsecureRequestWarning
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
@ -256,7 +257,8 @@ def generate_csr(domains, key_path, csr_path, key_type=RSA_KEY_TYPE):
def read_certificate(cert_path):
"""
Load the certificate from the provided path, and return a human readable version of it (TEXT mode).
Load the certificate from the provided path, and return a human readable version
of it (TEXT mode).
:param str cert_path: the path to the certificate
:returns: the TEXT version of the certificate, as it would be displayed by openssl binary
"""

View file

@ -1,3 +1,5 @@
# pylint: disable=missing-module-docstring
import json
import os
import stat
@ -5,18 +7,19 @@ import stat
import pkg_resources
import requests
from certbot_integration_tests.utils.constants import MOCK_OCSP_SERVER_PORT
from certbot_integration_tests.utils.constants import DEFAULT_HTTP_01_PORT, MOCK_OCSP_SERVER_PORT
PEBBLE_VERSION = 'v2.3.0'
ASSETS_PATH = pkg_resources.resource_filename('certbot_integration_tests', 'assets')
def fetch(workspace):
def fetch(workspace, http_01_port=DEFAULT_HTTP_01_PORT):
# pylint: disable=missing-function-docstring
suffix = 'linux-amd64' if os.name != 'nt' else 'windows-amd64.exe'
pebble_path = _fetch_asset('pebble', suffix)
challtestsrv_path = _fetch_asset('pebble-challtestsrv', suffix)
pebble_config_path = _build_pebble_config(workspace)
pebble_config_path = _build_pebble_config(workspace, http_01_port)
return pebble_path, challtestsrv_path, pebble_config_path
@ -35,7 +38,7 @@ def _fetch_asset(asset, suffix):
return asset_path
def _build_pebble_config(workspace):
def _build_pebble_config(workspace, http_01_port):
config_path = os.path.join(workspace, 'pebble-config.json')
with open(config_path, 'w') as file_h:
file_h.write(json.dumps({
@ -44,7 +47,7 @@ def _build_pebble_config(workspace):
'managementListenAddress': '0.0.0.0:15000',
'certificate': os.path.join(ASSETS_PATH, 'cert.pem'),
'privateKey': os.path.join(ASSETS_PATH, 'key.pem'),
'httpPort': 5002,
'httpPort': http_01_port,
'tlsPort': 5001,
'ocspResponderURL': 'http://127.0.0.1:{0}'.format(MOCK_OCSP_SERVER_PORT),
},

View file

@ -21,6 +21,7 @@ from certbot_integration_tests.utils.misc import GracefulTCPServer
class _ProxyHandler(BaseHTTPServer.BaseHTTPRequestHandler):
# pylint: disable=missing-function-docstring
def do_POST(self):
request = requests.get(PEBBLE_MANAGEMENT_URL + '/intermediate-keys/0', verify=False)
issuer_key = serialization.load_pem_private_key(request.content, None, default_backend())
@ -35,20 +36,28 @@ class _ProxyHandler(BaseHTTPServer.BaseHTTPRequestHandler):
ocsp_request = ocsp.load_der_ocsp_request(self.rfile.read(content_len))
response = requests.get('{0}/cert-status-by-serial/{1}'.format(
PEBBLE_MANAGEMENT_URL, str(hex(ocsp_request.serial_number)).replace('0x', '')), verify=False)
PEBBLE_MANAGEMENT_URL, str(hex(ocsp_request.serial_number)).replace('0x', '')),
verify=False
)
if not response.ok:
ocsp_response = ocsp.OCSPResponseBuilder.build_unsuccessful(ocsp.OCSPResponseStatus.UNAUTHORIZED)
ocsp_response = ocsp.OCSPResponseBuilder.build_unsuccessful(
ocsp.OCSPResponseStatus.UNAUTHORIZED
)
else:
data = response.json()
now = datetime.datetime.utcnow()
cert = x509.load_pem_x509_certificate(data['Certificate'].encode(), default_backend())
if data['Status'] != 'Revoked':
ocsp_status, revocation_time, revocation_reason = ocsp.OCSPCertStatus.GOOD, None, None
ocsp_status = ocsp.OCSPCertStatus.GOOD
revocation_time = None
revocation_reason = None
else:
ocsp_status, revocation_reason = ocsp.OCSPCertStatus.REVOKED, x509.ReasonFlags.unspecified
revoked_at = re.sub(r'( \+\d{4}).*$', r'\1', data['RevokedAt']) # "... +0000 UTC" => "+0000"
ocsp_status = ocsp.OCSPCertStatus.REVOKED
revocation_reason = x509.ReasonFlags.unspecified
# "... +0000 UTC" => "+0000"
revoked_at = re.sub(r'( \+\d{4}).*$', r'\1', data['RevokedAt'])
revocation_time = parser.parse(revoked_at)
ocsp_response = ocsp.OCSPResponseBuilder().add_response(

View file

@ -1,4 +1,6 @@
#!/usr/bin/env python
# pylint: disable=missing-module-docstring
import json
import re
import sys
@ -10,7 +12,9 @@ from certbot_integration_tests.utils.misc import GracefulTCPServer
def _create_proxy(mapping):
# pylint: disable=missing-function-docstring
class ProxyHandler(BaseHTTPServer.BaseHTTPRequestHandler):
# pylint: disable=missing-class-docstring
def do_GET(self):
headers = {key.lower(): value for key, value in self.headers.items()}
backend = [backend for pattern, backend in mapping.items()

View file

@ -18,7 +18,7 @@ install_requires = [
'python-dateutil',
'pyyaml',
'requests',
'six',
'six'
]
# Add pywin32 on Windows platforms to handle low-level system calls.

View file

@ -9,8 +9,6 @@ See https://docs.pytest.org/en/latest/reference.html#hook-reference
from __future__ import print_function
import os
import pytest
ROOT_PATH = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))

View file

@ -5,7 +5,7 @@ from setuptools import __version__ as setuptools_version
from setuptools import find_packages
from setuptools import setup
version = '1.10.0.dev0'
version = '1.11.0.dev0'
install_requires = [
'certbot',

View file

@ -3,6 +3,10 @@ The `~certbot_dns_cloudflare.dns_cloudflare` plugin automates the process of
completing a ``dns-01`` challenge (`~acme.challenges.DNS01`) by creating, and
subsequently removing, TXT records using the Cloudflare API.
.. note::
The plugin is not installed by default. It can be installed by heading to
`certbot.eff.org <https://certbot.eff.org/instructions#wildcard>`_, choosing your system and
selecting the Wildcard tab.
Named Arguments
---------------

View file

@ -6,7 +6,7 @@ from setuptools import __version__ as setuptools_version
from setuptools import find_packages
from setuptools import setup
version = '1.10.0.dev0'
version = '1.11.0.dev0'
# Remember to update local-oldest-requirements.txt when changing the minimum
# acme/certbot version.

View file

@ -3,6 +3,10 @@ The `~certbot_dns_cloudxns.dns_cloudxns` plugin automates the process of
completing a ``dns-01`` challenge (`~acme.challenges.DNS01`) by creating, and
subsequently removing, TXT records using the CloudXNS API.
.. note::
The plugin is not installed by default. It can be installed by heading to
`certbot.eff.org <https://certbot.eff.org/instructions#wildcard>`_, choosing your system and
selecting the Wildcard tab.
Named Arguments
---------------

View file

@ -6,7 +6,7 @@ from setuptools import __version__ as setuptools_version
from setuptools import find_packages
from setuptools import setup
version = '1.10.0.dev0'
version = '1.11.0.dev0'
# Remember to update local-oldest-requirements.txt when changing the minimum
# acme/certbot version.

View file

@ -3,6 +3,10 @@ The `~certbot_dns_digitalocean.dns_digitalocean` plugin automates the process of
completing a ``dns-01`` challenge (`~acme.challenges.DNS01`) by creating, and
subsequently removing, TXT records using the DigitalOcean API.
.. note::
The plugin is not installed by default. It can be installed by heading to
`certbot.eff.org <https://certbot.eff.org/instructions#wildcard>`_, choosing your system and
selecting the Wildcard tab.
Named Arguments
---------------

View file

@ -19,7 +19,8 @@ class Authenticator(dns_common.DNSAuthenticator):
This Authenticator uses the DigitalOcean API to fulfill a dns-01 challenge.
"""
description = 'Obtain certs using a DNS TXT record (if you are using DigitalOcean for DNS).'
description = 'Obtain certificates using a DNS TXT record (if you are ' + \
'using DigitalOcean for DNS).'
def __init__(self, *args, **kwargs):
super(Authenticator, self).__init__(*args, **kwargs)

View file

@ -6,7 +6,7 @@ from setuptools import __version__ as setuptools_version
from setuptools import find_packages
from setuptools import setup
version = '1.10.0.dev0'
version = '1.11.0.dev0'
# Remember to update local-oldest-requirements.txt when changing the minimum
# acme/certbot version.

View file

@ -3,6 +3,10 @@ The `~certbot_dns_dnsimple.dns_dnsimple` plugin automates the process of
completing a ``dns-01`` challenge (`~acme.challenges.DNS01`) by creating, and
subsequently removing, TXT records using the DNSimple API.
.. note::
The plugin is not installed by default. It can be installed by heading to
`certbot.eff.org <https://certbot.eff.org/instructions#wildcard>`_, choosing your system and
selecting the Wildcard tab.
Named Arguments
---------------

View file

@ -6,7 +6,7 @@ from setuptools import __version__ as setuptools_version
from setuptools import find_packages
from setuptools import setup
version = '1.10.0.dev0'
version = '1.11.0.dev0'
# Remember to update local-oldest-requirements.txt when changing the minimum
# acme/certbot version.

View file

@ -3,6 +3,10 @@ The `~certbot_dns_dnsmadeeasy.dns_dnsmadeeasy` plugin automates the process of
completing a ``dns-01`` challenge (`~acme.challenges.DNS01`) by creating, and
subsequently removing, TXT records using the DNS Made Easy API.
.. note::
The plugin is not installed by default. It can be installed by heading to
`certbot.eff.org <https://certbot.eff.org/instructions#wildcard>`_, choosing your system and
selecting the Wildcard tab.
Named Arguments
---------------

View file

@ -6,7 +6,7 @@ from setuptools import __version__ as setuptools_version
from setuptools import find_packages
from setuptools import setup
version = '1.10.0.dev0'
version = '1.11.0.dev0'
# Remember to update local-oldest-requirements.txt when changing the minimum
# acme/certbot version.

View file

@ -3,6 +3,10 @@ The `~certbot_dns_gehirn.dns_gehirn` plugin automates the process of completing
a ``dns-01`` challenge (`~acme.challenges.DNS01`) by creating, and subsequently
removing, TXT records using the Gehirn Infrastructure Service DNS API.
.. note::
The plugin is not installed by default. It can be installed by heading to
`certbot.eff.org <https://certbot.eff.org/instructions#wildcard>`_, choosing your system and
selecting the Wildcard tab.
Named Arguments
---------------

View file

@ -6,7 +6,7 @@ from setuptools import __version__ as setuptools_version
from setuptools import find_packages
from setuptools import setup
version = '1.10.0.dev0'
version = '1.11.0.dev0'
# Please update tox.ini when modifying dependency version requirements
install_requires = [

View file

@ -3,6 +3,10 @@ The `~certbot_dns_google.dns_google` plugin automates the process of
completing a ``dns-01`` challenge (`~acme.challenges.DNS01`) by creating, and
subsequently removing, TXT records using the Google Cloud DNS API.
.. note::
The plugin is not installed by default. It can be installed by heading to
`certbot.eff.org <https://certbot.eff.org/instructions#wildcard>`_, choosing your system and
selecting the Wildcard tab.
Named Arguments
---------------

View file

@ -85,9 +85,13 @@ class _GoogleClient(object):
scopes = ['https://www.googleapis.com/auth/ndev.clouddns.readwrite']
if account_json is not None:
credentials = ServiceAccountCredentials.from_json_keyfile_name(account_json, scopes)
with open(account_json) as account:
self.project_id = json.load(account)['project_id']
try:
credentials = ServiceAccountCredentials.from_json_keyfile_name(account_json, scopes)
with open(account_json) as account:
self.project_id = json.load(account)['project_id']
except Exception as e:
raise errors.PluginError(
"Error parsing credentials file '{}': {}".format(account_json, e))
else:
credentials = None
self.project_id = self.get_project_id()
@ -114,10 +118,13 @@ class _GoogleClient(object):
record_contents = self.get_existing_txt_rrset(zone_id, record_name)
if record_contents is None:
record_contents = []
add_records = record_contents[:]
# If it wasn't possible to fetch the records at this label (missing .list permission),
# assume there aren't any (#5678). If there are actually records here, this will fail
# with HTTP 409/412 API errors.
record_contents = {"rrdatas": []}
add_records = record_contents["rrdatas"][:]
if "\""+record_content+"\"" in record_contents:
if "\""+record_content+"\"" in record_contents["rrdatas"]:
# The process was interrupted previously and validation token exists
return
@ -136,15 +143,15 @@ class _GoogleClient(object):
],
}
if record_contents:
if record_contents["rrdatas"]:
# We need to remove old records in the same request
data["deletions"] = [
{
"kind": "dns#resourceRecordSet",
"type": "TXT",
"name": record_name + ".",
"rrdatas": record_contents,
"ttl": record_ttl,
"rrdatas": record_contents["rrdatas"],
"ttl": record_contents["ttl"],
},
]
@ -184,7 +191,10 @@ class _GoogleClient(object):
record_contents = self.get_existing_txt_rrset(zone_id, record_name)
if record_contents is None:
record_contents = ["\"" + record_content + "\""]
# If it wasn't possible to fetch the records at this label (missing .list permission),
# assume there aren't any (#5678). If there are actually records here, this will fail
# with HTTP 409/412 API errors.
record_contents = {"rrdatas": ["\"" + record_content + "\""], "ttl": record_ttl}
data = {
"kind": "dns#change",
@ -193,14 +203,15 @@ class _GoogleClient(object):
"kind": "dns#resourceRecordSet",
"type": "TXT",
"name": record_name + ".",
"rrdatas": record_contents,
"ttl": record_ttl,
"rrdatas": record_contents["rrdatas"],
"ttl": record_contents["ttl"],
},
],
}
# Remove the record being deleted from the list
readd_contents = [r for r in record_contents if r != "\"" + record_content + "\""]
readd_contents = [r for r in record_contents["rrdatas"]
if r != "\"" + record_content + "\""]
if readd_contents:
# We need to remove old records in the same request
data["additions"] = [
@ -209,7 +220,7 @@ class _GoogleClient(object):
"type": "TXT",
"name": record_name + ".",
"rrdatas": readd_contents,
"ttl": record_ttl,
"ttl": record_contents["ttl"],
},
]
@ -231,14 +242,15 @@ class _GoogleClient(object):
:param str zone_id: The ID of the managed zone.
:param str record_name: The record name (typically beginning with '_acme-challenge.').
:returns: List of TXT record values or None
:rtype: `list` of `string` or `None`
:returns: The resourceRecordSet corresponding to `record_name` or None
:rtype: `resourceRecordSet <https://cloud.google.com/dns/docs/reference/v1/resourceRecordSets#resource>` or `None` # pylint: disable=line-too-long
"""
rrs_request = self.dns.resourceRecordSets()
request = rrs_request.list(managedZone=zone_id, project=self.project_id)
# Add dot as the API returns absolute domains
record_name += "."
request = rrs_request.list(project=self.project_id, managedZone=zone_id, name=record_name,
type="TXT")
try:
response = request.execute()
except googleapiclient_errors.Error:
@ -246,10 +258,8 @@ class _GoogleClient(object):
"requesting a wildcard certificate, this might not work.")
logger.debug("Error was:", exc_info=True)
else:
if response:
for rr in response["rrsets"]:
if rr["name"] == record_name and rr["type"] == "TXT":
return rr["rrdatas"]
if response and response["rrsets"]:
return response["rrsets"][0]
return None
def _find_managed_zone_id(self, domain):

View file

@ -6,7 +6,7 @@ from setuptools import __version__ as setuptools_version
from setuptools import find_packages
from setuptools import setup
version = '1.10.0.dev0'
version = '1.11.0.dev0'
# Remember to update local-oldest-requirements.txt when changing the minimum
# acme/certbot version.

View file

@ -70,7 +70,7 @@ class GoogleClientTest(unittest.TestCase):
zone = "ZONE_ID"
change = "an-id"
def _setUp_client_with_mock(self, zone_request_side_effect):
def _setUp_client_with_mock(self, zone_request_side_effect, rrs_list_side_effect=None):
from certbot_dns_google._internal.dns_google import _GoogleClient
pwd = os.path.dirname(__file__)
@ -86,9 +86,16 @@ class GoogleClientTest(unittest.TestCase):
mock_mz.list.return_value.execute.side_effect = zone_request_side_effect
mock_rrs = mock.MagicMock()
rrsets = {"rrsets": [{"name": "_acme-challenge.example.org.", "type": "TXT",
"rrdatas": ["\"example-txt-contents\""]}]}
mock_rrs.list.return_value.execute.return_value = rrsets
def rrs_list(project=None, managedZone=None, name=None, type=None):
response = {"rrsets": []}
if name == "_acme-challenge.example.org.":
response = {"rrsets": [{"name": "_acme-challenge.example.org.", "type": "TXT",
"rrdatas": ["\"example-txt-contents\""], "ttl": 60}]}
mock_return = mock.MagicMock()
mock_return.execute.return_value = response
mock_return.execute.side_effect = rrs_list_side_effect
return mock_return
mock_rrs.list.side_effect = rrs_list
mock_changes = mock.MagicMock()
client.dns.managedZones = mock.MagicMock(return_value=mock_mz)
@ -107,6 +114,17 @@ class GoogleClientTest(unittest.TestCase):
self.assertFalse(credential_mock.called)
self.assertTrue(get_project_id_mock.called)
@mock.patch('oauth2client.service_account.ServiceAccountCredentials.from_json_keyfile_name')
def test_client_bad_credentials_file(self, credential_mock):
credential_mock.side_effect = ValueError('Some exception buried in oauth2client')
with self.assertRaises(errors.PluginError) as cm:
self._setUp_client_with_mock([])
self.assertEqual(
str(cm.exception),
"Error parsing credentials file '/not/a/real/path.json': "
"Some exception buried in oauth2client"
)
@mock.patch('oauth2client.service_account.ServiceAccountCredentials.from_json_keyfile_name')
@mock.patch('certbot_dns_google._internal.dns_google.open',
mock.mock_open(read_data='{"project_id": "' + PROJECT_ID + '"}'), create=True)
@ -162,11 +180,29 @@ class GoogleClientTest(unittest.TestCase):
# pylint: disable=line-too-long
mock_get_rrs = "certbot_dns_google._internal.dns_google._GoogleClient.get_existing_txt_rrset"
with mock.patch(mock_get_rrs) as mock_rrs:
mock_rrs.return_value = ["sample-txt-contents"]
mock_rrs.return_value = {"rrdatas": ["sample-txt-contents"], "ttl": self.record_ttl}
client.add_txt_record(DOMAIN, self.record_name, self.record_content, self.record_ttl)
self.assertTrue(changes.create.called)
self.assertTrue("sample-txt-contents" in
changes.create.call_args_list[0][1]["body"]["deletions"][0]["rrdatas"])
deletions = changes.create.call_args_list[0][1]["body"]["deletions"][0]
self.assertTrue("sample-txt-contents" in deletions["rrdatas"])
self.assertEqual(self.record_ttl, deletions["ttl"])
@mock.patch('oauth2client.service_account.ServiceAccountCredentials.from_json_keyfile_name')
@mock.patch('certbot_dns_google._internal.dns_google.open',
mock.mock_open(read_data='{"project_id": "' + PROJECT_ID + '"}'), create=True)
def test_add_txt_record_delete_old_ttl_case(self, unused_credential_mock):
client, changes = self._setUp_client_with_mock(
[{'managedZones': [{'id': self.zone}]}])
# pylint: disable=line-too-long
mock_get_rrs = "certbot_dns_google._internal.dns_google._GoogleClient.get_existing_txt_rrset"
with mock.patch(mock_get_rrs) as mock_rrs:
custom_ttl = 300
mock_rrs.return_value = {"rrdatas": ["sample-txt-contents"], "ttl": custom_ttl}
client.add_txt_record(DOMAIN, self.record_name, self.record_content, self.record_ttl)
self.assertTrue(changes.create.called)
deletions = changes.create.call_args_list[0][1]["body"]["deletions"][0]
self.assertTrue("sample-txt-contents" in deletions["rrdatas"])
self.assertEqual(custom_ttl, deletions["ttl"]) #otherwise HTTP 412
@mock.patch('oauth2client.service_account.ServiceAccountCredentials.from_json_keyfile_name')
@mock.patch('certbot_dns_google._internal.dns_google.open',
@ -210,14 +246,13 @@ class GoogleClientTest(unittest.TestCase):
@mock.patch('oauth2client.service_account.ServiceAccountCredentials.from_json_keyfile_name')
@mock.patch('certbot_dns_google._internal.dns_google.open',
mock.mock_open(read_data='{"project_id": "' + PROJECT_ID + '"}'), create=True)
def test_del_txt_record(self, unused_credential_mock):
def test_del_txt_record_multi_rrdatas(self, unused_credential_mock):
client, changes = self._setUp_client_with_mock([{'managedZones': [{'id': self.zone}]}])
# pylint: disable=line-too-long
mock_get_rrs = "certbot_dns_google._internal.dns_google._GoogleClient.get_existing_txt_rrset"
with mock.patch(mock_get_rrs) as mock_rrs:
mock_rrs.return_value = ["\"sample-txt-contents\"",
"\"example-txt-contents\""]
mock_rrs.return_value = {"rrdatas": ["\"sample-txt-contents\"",
"\"example-txt-contents\""], "ttl": self.record_ttl}
client.del_txt_record(DOMAIN, "_acme-challenge.example.org",
"example-txt-contents", self.record_ttl)
@ -250,19 +285,48 @@ class GoogleClientTest(unittest.TestCase):
@mock.patch('oauth2client.service_account.ServiceAccountCredentials.from_json_keyfile_name')
@mock.patch('certbot_dns_google._internal.dns_google.open',
mock.mock_open(read_data='{"project_id": "' + PROJECT_ID + '"}'), create=True)
def test_del_txt_record_error_during_zone_lookup(self, unused_credential_mock):
client, unused_changes = self._setUp_client_with_mock(API_ERROR)
def test_del_txt_record_single_rrdatas(self, unused_credential_mock):
client, changes = self._setUp_client_with_mock([{'managedZones': [{'id': self.zone}]}])
# pylint: disable=line-too-long
mock_get_rrs = "certbot_dns_google._internal.dns_google._GoogleClient.get_existing_txt_rrset"
with mock.patch(mock_get_rrs) as mock_rrs:
mock_rrs.return_value = {"rrdatas": ["\"example-txt-contents\""], "ttl": self.record_ttl}
client.del_txt_record(DOMAIN, "_acme-challenge.example.org",
"example-txt-contents", self.record_ttl)
expected_body = {
"kind": "dns#change",
"deletions": [
{
"kind": "dns#resourceRecordSet",
"type": "TXT",
"name": "_acme-challenge.example.org.",
"rrdatas": ["\"example-txt-contents\""],
"ttl": self.record_ttl,
},
],
}
changes.create.assert_called_with(body=expected_body,
managedZone=self.zone,
project=PROJECT_ID)
@mock.patch('oauth2client.service_account.ServiceAccountCredentials.from_json_keyfile_name')
@mock.patch('certbot_dns_google._internal.dns_google.open',
mock.mock_open(read_data='{"project_id": "' + PROJECT_ID + '"}'), create=True)
def test_del_txt_record_error_during_zone_lookup(self, unused_credential_mock):
client, changes = self._setUp_client_with_mock(API_ERROR)
client.del_txt_record(DOMAIN, self.record_name, self.record_content, self.record_ttl)
changes.create.assert_not_called()
@mock.patch('oauth2client.service_account.ServiceAccountCredentials.from_json_keyfile_name')
@mock.patch('certbot_dns_google._internal.dns_google.open',
mock.mock_open(read_data='{"project_id": "' + PROJECT_ID + '"}'), create=True)
def test_del_txt_record_zone_not_found(self, unused_credential_mock):
client, unused_changes = self._setUp_client_with_mock([{'managedZones': []},
client, changes = self._setUp_client_with_mock([{'managedZones': []},
{'managedZones': []}])
client.del_txt_record(DOMAIN, self.record_name, self.record_content, self.record_ttl)
changes.create.assert_not_called()
@mock.patch('oauth2client.service_account.ServiceAccountCredentials.from_json_keyfile_name')
@mock.patch('certbot_dns_google._internal.dns_google.open',
@ -276,24 +340,39 @@ class GoogleClientTest(unittest.TestCase):
@mock.patch('oauth2client.service_account.ServiceAccountCredentials.from_json_keyfile_name')
@mock.patch('certbot_dns_google._internal.dns_google.open',
mock.mock_open(read_data='{"project_id": "' + PROJECT_ID + '"}'), create=True)
def test_get_existing(self, unused_credential_mock):
def test_get_existing_found(self, unused_credential_mock):
client, unused_changes = self._setUp_client_with_mock(
[{'managedZones': [{'id': self.zone}]}])
# Record name mocked in setUp
found = client.get_existing_txt_rrset(self.zone, "_acme-challenge.example.org")
self.assertEqual(found, ["\"example-txt-contents\""])
self.assertEqual(found["rrdatas"], ["\"example-txt-contents\""])
self.assertEqual(found["ttl"], 60)
@mock.patch('oauth2client.service_account.ServiceAccountCredentials.from_json_keyfile_name')
@mock.patch('certbot_dns_google._internal.dns_google.open',
mock.mock_open(read_data='{"project_id": "' + PROJECT_ID + '"}'), create=True)
def test_get_existing_not_found(self, unused_credential_mock):
client, unused_changes = self._setUp_client_with_mock(
[{'managedZones': [{'id': self.zone}]}])
not_found = client.get_existing_txt_rrset(self.zone, "nonexistent.tld")
self.assertEqual(not_found, None)
@mock.patch('oauth2client.service_account.ServiceAccountCredentials.from_json_keyfile_name')
@mock.patch('certbot_dns_google._internal.dns_google.open',
mock.mock_open(read_data='{"project_id": "' + PROJECT_ID + '"}'), create=True)
def test_get_existing_with_error(self, unused_credential_mock):
client, unused_changes = self._setUp_client_with_mock(
[{'managedZones': [{'id': self.zone}]}], API_ERROR)
# Record name mocked in setUp
found = client.get_existing_txt_rrset(self.zone, "_acme-challenge.example.org")
self.assertEqual(found, None)
@mock.patch('oauth2client.service_account.ServiceAccountCredentials.from_json_keyfile_name')
@mock.patch('certbot_dns_google._internal.dns_google.open',
mock.mock_open(read_data='{"project_id": "' + PROJECT_ID + '"}'), create=True)
def test_get_existing_fallback(self, unused_credential_mock):
client, unused_changes = self._setUp_client_with_mock(
[{'managedZones': [{'id': self.zone}]}])
mock_execute = client.dns.resourceRecordSets.return_value.list.return_value.execute
mock_execute.side_effect = API_ERROR
[{'managedZones': [{'id': self.zone}]}], API_ERROR)
rrset = client.get_existing_txt_rrset(self.zone, "_acme-challenge.example.org")
self.assertFalse(rrset)

View file

@ -3,6 +3,10 @@ The `~certbot_dns_linode.dns_linode` plugin automates the process of
completing a ``dns-01`` challenge (`~acme.challenges.DNS01`) by creating, and
subsequently removing, TXT records using the Linode API.
.. note::
The plugin is not installed by default. It can be installed by heading to
`certbot.eff.org <https://certbot.eff.org/instructions#wildcard>`_, choosing your system and
selecting the Wildcard tab.
Named Arguments
---------------

View file

@ -24,7 +24,7 @@ class Authenticator(dns_common.DNSAuthenticator):
This Authenticator uses the Linode API to fulfill a dns-01 challenge.
"""
description = 'Obtain certs using a DNS TXT record (if you are using Linode for DNS).'
description = 'Obtain certificates using a DNS TXT record (if you are using Linode for DNS).'
def __init__(self, *args, **kwargs):
super(Authenticator, self).__init__(*args, **kwargs)

View file

@ -6,7 +6,7 @@ from setuptools import __version__ as setuptools_version
from setuptools import find_packages
from setuptools import setup
version = '1.10.0.dev0'
version = '1.11.0.dev0'
# Please update tox.ini when modifying dependency version requirements
install_requires = [

View file

@ -3,6 +3,10 @@ The `~certbot_dns_luadns.dns_luadns` plugin automates the process of
completing a ``dns-01`` challenge (`~acme.challenges.DNS01`) by creating, and
subsequently removing, TXT records using the LuaDNS API.
.. note::
The plugin is not installed by default. It can be installed by heading to
`certbot.eff.org <https://certbot.eff.org/instructions#wildcard>`_, choosing your system and
selecting the Wildcard tab.
Named Arguments
---------------

View file

@ -6,7 +6,7 @@ from setuptools import __version__ as setuptools_version
from setuptools import find_packages
from setuptools import setup
version = '1.10.0.dev0'
version = '1.11.0.dev0'
# Remember to update local-oldest-requirements.txt when changing the minimum
# acme/certbot version.

View file

@ -3,6 +3,10 @@ The `~certbot_dns_nsone.dns_nsone` plugin automates the process of completing
a ``dns-01`` challenge (`~acme.challenges.DNS01`) by creating, and subsequently
removing, TXT records using the NS1 API.
.. note::
The plugin is not installed by default. It can be installed by heading to
`certbot.eff.org <https://certbot.eff.org/instructions#wildcard>`_, choosing your system and
selecting the Wildcard tab.
Named Arguments
---------------

View file

@ -6,7 +6,7 @@ from setuptools import __version__ as setuptools_version
from setuptools import find_packages
from setuptools import setup
version = '1.10.0.dev0'
version = '1.11.0.dev0'
# Remember to update local-oldest-requirements.txt when changing the minimum
# acme/certbot version.

View file

@ -3,6 +3,10 @@ The `~certbot_dns_ovh.dns_ovh` plugin automates the process of
completing a ``dns-01`` challenge (`~acme.challenges.DNS01`) by creating, and
subsequently removing, TXT records using the OVH API.
.. note::
The plugin is not installed by default. It can be installed by heading to
`certbot.eff.org <https://certbot.eff.org/instructions#wildcard>`_, choosing your system and
selecting the Wildcard tab.
Named Arguments
---------------

View file

@ -6,7 +6,7 @@ from setuptools import __version__ as setuptools_version
from setuptools import find_packages
from setuptools import setup
version = '1.10.0.dev0'
version = '1.11.0.dev0'
# Remember to update local-oldest-requirements.txt when changing the minimum
# acme/certbot version.

View file

@ -3,6 +3,10 @@ The `~certbot_dns_rfc2136.dns_rfc2136` plugin automates the process of
completing a ``dns-01`` challenge (`~acme.challenges.DNS01`) by creating, and
subsequently removing, TXT records using RFC 2136 Dynamic Updates.
.. note::
The plugin is not installed by default. It can be installed by heading to
`certbot.eff.org <https://certbot.eff.org/instructions#wildcard>`_, choosing your system and
selecting the Wildcard tab.
Named Arguments
---------------

View file

@ -6,7 +6,7 @@ from setuptools import __version__ as setuptools_version
from setuptools import find_packages
from setuptools import setup
version = '1.10.0.dev0'
version = '1.11.0.dev0'
# Remember to update local-oldest-requirements.txt when changing the minimum
# acme/certbot version.

View file

@ -3,6 +3,10 @@ The `~certbot_dns_route53.dns_route53` plugin automates the process of
completing a ``dns-01`` challenge (`~acme.challenges.DNS01`) by creating, and
subsequently removing, TXT records using the Amazon Web Services Route 53 API.
.. note::
The plugin is not installed by default. It can be installed by heading to
`certbot.eff.org <https://certbot.eff.org/instructions#wildcard>`_, choosing your system and
selecting the Wildcard tab.
Named Arguments
---------------

View file

@ -6,7 +6,7 @@ from setuptools import __version__ as setuptools_version
from setuptools import find_packages
from setuptools import setup
version = '1.10.0.dev0'
version = '1.11.0.dev0'
# Remember to update local-oldest-requirements.txt when changing the minimum
# acme/certbot version.

View file

@ -3,6 +3,10 @@ The `~certbot_dns_sakuracloud.dns_sakuracloud` plugin automates the process of c
a ``dns-01`` challenge (`~acme.challenges.DNS01`) by creating, and subsequently
removing, TXT records using the Sakura Cloud DNS API.
.. note::
The plugin is not installed by default. It can be installed by heading to
`certbot.eff.org <https://certbot.eff.org/instructions#wildcard>`_, choosing your system and
selecting the Wildcard tab.
Named Arguments
---------------

View file

@ -6,7 +6,7 @@ from setuptools import __version__ as setuptools_version
from setuptools import find_packages
from setuptools import setup
version = '1.10.0.dev0'
version = '1.11.0.dev0'
# Please update tox.ini when modifying dependency version requirements
install_requires = [

View file

@ -226,7 +226,7 @@ class NginxConfigurator(common.Installer):
if not fullchain_path:
raise errors.PluginError(
"The nginx plugin currently requires --fullchain-path to "
"install a cert.")
"install a certificate.")
vhosts = self.choose_vhosts(domain, create_if_no_match=True)
for vhost in vhosts:

View file

@ -1,5 +1,6 @@
"""A class that performs HTTP-01 challenges for Nginx"""
import io
import logging
from acme import challenges
@ -102,7 +103,7 @@ class NginxHttp01(common.ChallengePerformer):
self.configurator.reverter.register_file_creation(
True, self.challenge_conf)
with open(self.challenge_conf, "w") as new_conf:
with io.open(self.challenge_conf, "w", encoding="utf-8") as new_conf:
nginxparser.dump(config, new_conf)
def _default_listen_addresses(self):

View file

@ -16,6 +16,7 @@ from pyparsing import stringEnd
from pyparsing import White
from pyparsing import ZeroOrMore
import six
from acme.magic_typing import IO, Any # pylint: disable=unused-import
logger = logging.getLogger(__name__)
@ -130,26 +131,27 @@ def load(_file):
def dumps(blocks):
"""Dump to a string.
# type: (UnspacedList) -> six.text_type
"""Dump to a Unicode string.
:param UnspacedList block: The parsed tree
:param int indentation: The number of spaces to indent
:rtype: str
:rtype: six.text_type
"""
return str(RawNginxDumper(blocks.spaced))
return six.text_type(RawNginxDumper(blocks.spaced))
def dump(blocks, _file):
# type: (UnspacedList, IO[Any]) -> None
"""Dump to a file.
:param UnspacedList block: The parsed tree
:param file _file: The file to dump to
:param int indentation: The number of spaces to indent
:rtype: NoneType
:param IO[Any] _file: The file stream to dump to. It must be opened with
Unicode encoding.
:rtype: None
"""
return _file.write(dumps(blocks))
_file.write(dumps(blocks))
spacey = lambda x: (isinstance(x, six.string_types) and x.isspace()) or x == ''

View file

@ -249,7 +249,7 @@ class NginxParser(object):
continue
out = nginxparser.dumps(tree)
logger.debug('Writing nginx conf tree to %s:\n%s', filename, out)
with open(filename, 'w') as _file:
with io.open(filename, 'w', encoding='utf-8') as _file:
_file.write(out)
except IOError:

View file

@ -5,7 +5,7 @@ from setuptools import __version__ as setuptools_version
from setuptools import find_packages
from setuptools import setup
version = '1.10.0.dev0'
version = '1.11.0.dev0'
# Remember to update local-oldest-requirements.txt when changing the minimum
# acme/certbot version.

View file

@ -492,6 +492,14 @@ class NginxParserTest(util.NginxTest):
self.assertEqual(['server'], parsed[0][2][0])
self.assertEqual(['listen', '80'], parsed[0][2][1][3])
def test_valid_unicode_roundtrip(self):
"""This tests the parser's ability to load and save a config containing Unicode"""
nparser = parser.NginxParser(self.config_path)
nparser._parse_files(
nparser.abs_path('valid_unicode_comments.conf')
) # pylint: disable=protected-access
nparser.filedump(lazy=False)
def test_invalid_unicode_characters(self):
with self.assertLogs() as log:
nparser = parser.NginxParser(self.config_path)

View file

@ -2,7 +2,42 @@
Certbot adheres to [Semantic Versioning](https://semver.org/).
## 1.10.0 - master
## 1.11.0 - master
### Added
*
### Changed
* We deprecated support for Python 2 in Certbot and its ACME library.
Support for Python 2 will be removed in the next planned release of Certbot.
* certbot-auto was deprecated on all systems. For more information about this
change, see
https://community.letsencrypt.org/t/certbot-auto-no-longer-works-on-debian-based-systems/139702/7.
* We deprecated support for Apache 2.2 in the certbot-apache plugin and it will
be removed in a future release of Certbot.
### Fixed
* The Certbot snap no longer loads packages installed via `pip install --user`. This
was unintended and DNS plugins should be installed via `snap` instead.
* `certbot-dns-google` would sometimes crash with HTTP 409/412 errors when used with very large zones. See [#6036](https://github.com/certbot/certbot/issues/6036).
* `certbot-dns-google` would sometimes crash with an HTTP 412 error if preexisting records had an unexpected TTL, i.e.: different than Certbot's default TTL for this plugin. See [#8551](https://github.com/certbot/certbot/issues/8551).
More details about these changes can be found on our GitHub repo.
## 1.10.1 - 2020-12-03
### Fixed
* Fixed a bug in `certbot.util.add_deprecated_argument` that caused the
deprecated `--manual-public-ip-logging-ok` flag to crash Certbot in some
scenarios.
More details about these changes can be found on our GitHub repo.
## 1.10.0 - 2020-12-01
### Added
@ -10,7 +45,7 @@ Certbot adheres to [Semantic Versioning](https://semver.org/).
* Confirmation when deleting certificates
* CLI flag `--key-type` has been added to specify 'rsa' or 'ecdsa' (default 'rsa').
* CLI flag `--elliptic-curve` has been added which takes an NIST/SECG elliptic curve. Any of
`secp256r1`, `secp284r1` and `secp521r1` are accepted values.
`secp256r1`, `secp384r1` and `secp521r1` are accepted values.
* The command `certbot certficates` lists the which type of the private key that was used
for the private key.
* Support for Python 3.9 was added to Certbot and all of its components.
@ -20,11 +55,10 @@ Certbot adheres to [Semantic Versioning](https://semver.org/).
* certbot-auto was deprecated on Debian based systems.
* CLI flag `--manual-public-ip-logging-ok` is now a no-op, generates a
deprecation warning, and will be removed in a future release.
*
### Fixed
*
* Fixed a Unicode-related crash in the nginx plugin when running under Python 2.
More details about these changes can be found on our GitHub repo.

View file

@ -18,10 +18,6 @@ systems.
To see the changes made to Certbot between versions please refer to our
`changelog <https://github.com/certbot/certbot/blob/master/certbot/CHANGELOG.md>`_.
Until May 2016, Certbot was named simply ``letsencrypt`` or ``letsencrypt-auto``,
depending on install method. Instructions on the Internet, and some pieces of the
software, may still refer to this older name.
Contributing
------------
@ -96,7 +92,7 @@ Current Features
- apache/2.x
- nginx/0.8.48+
- webroot (adds files to webroot directories in order to prove control of
domains and obtain certs)
domains and obtain certificates)
- standalone (runs its own simple webserver to prove you control a domain)
- other server software via `third party plugins <https://certbot.eff.org/docs/using.html#third-party-plugins>`_
@ -106,7 +102,7 @@ Current Features
* Can get domain-validated (DV) certificates.
* Can revoke certificates.
* Adjustable RSA key bit-length (2048 (default), 4096, ...).
* Adjustable [EC](https://en.wikipedia.org/wiki/Elliptic-curve_cryptography)
* Adjustable `EC <https://en.wikipedia.org/wiki/Elliptic-curve_cryptography>`_
key (`secp256r1` (default), `secp384r1`, `secp521r1`).
* Can optionally install a http -> https redirect, so your site effectively
runs https only (Apache only)

View file

@ -1,4 +1,13 @@
"""Certbot client."""
import warnings
import sys
# version number like 1.2.3a0, must have at least 2 parts, like 1.2
__version__ = '1.10.0.dev0'
__version__ = '1.11.0.dev0'
if sys.version_info[0] == 2:
warnings.warn(
"Python 2 support will be dropped in the next release of Certbot. "
"Please upgrade your Python version.",
PendingDeprecationWarning,
) # pragma: no cover

View file

@ -369,7 +369,7 @@ def _describe_certs(config, parsed_certs, parse_failures):
notify = out.append
if not parsed_certs and not parse_failures:
notify("No certs found.")
notify("No certificates found.")
else:
if parsed_certs:
match = "matching " if config.certname or config.domains else ""

View file

@ -2,8 +2,10 @@
from __future__ import print_function
import argparse
import copy
import functools
import glob
import sys
import configargparse
import six
import zope.component
@ -356,6 +358,18 @@ class HelpfulArgumentParser(object):
:param dict **kwargs: various argparse settings for this argument
"""
action = kwargs.get("action")
if action is util.DeprecatedArgumentAction:
# If the argument is deprecated through
# certbot.util.add_deprecated_argument, it is not shown in the help
# output and any value given to the argument is thrown away during
# argument parsing. Because of this, we handle this case early
# skipping putting the argument in different help topics and
# handling default detection since these actions aren't needed and
# can cause bugs like
# https://github.com/certbot/certbot/issues/8495.
self.parser.add_argument(*args, **kwargs)
return
if isinstance(topics, list):
# if this flag can be listed in multiple sections, try to pick the one
@ -410,8 +424,22 @@ class HelpfulArgumentParser(object):
:param int nargs: Number of arguments the option takes.
"""
util.add_deprecated_argument(
self.parser.add_argument, argument_name, num_args)
# certbot.util.add_deprecated_argument expects the normal add_argument
# interface provided by argparse. This is what is given including when
# certbot.util.add_deprecated_argument is used by plugins, however, in
# that case the first argument to certbot.util.add_deprecated_argument
# is certbot._internal.cli.HelpfulArgumentGroup.add_argument which
# internally calls the add method of this class.
#
# The difference between the add method of this class and the standard
# argparse add_argument method caused a bug in the past (see
# https://github.com/certbot/certbot/issues/8495) so we use the same
# code path here for consistency and to ensure it works. To do that, we
# wrap the add method in a similar way to
# HelpfulArgumentGroup.add_argument by providing a help topic (which in
# this case is set to None).
add_func = functools.partial(self.add, None)
util.add_deprecated_argument(add_func, argument_name, num_args)
def add_group(self, topic, verbs=(), **kwargs):
"""Create a new argument group.

View file

@ -5,6 +5,7 @@ from __future__ import print_function
import functools
import logging.handlers
import sys
import warnings
import configobj
import josepy as jose
@ -253,7 +254,7 @@ def _handle_identical_cert_request(config, # type: configuration.NamespaceConfi
elif config.verb == "certonly":
keep_opt = "Keep the existing certificate for now"
choices = [keep_opt,
"Renew & replace the cert (may be subject to CA rate limits)"]
"Renew & replace the certificate (may be subject to CA rate limits)"]
display = zope.component.getUtility(interfaces.IDisplay)
response = display.menu(question, choices,
@ -433,8 +434,8 @@ def _ask_user_to_confirm_new_names(config, new_domains, certname, old_domains):
_format_list("-", removed),
br=os.linesep))
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.")
if not obj.yesno(msg, "Update certificate", "Cancel", default=True):
raise errors.ConfigurationError("Specified mismatched certificate name and domains.")
def _find_domains_or_certname(config, installer, question=None):
@ -512,7 +513,7 @@ def _report_new_cert(config, cert_path, fullchain_path, key_path=None):
# and say something more informative here.
msg = ('Congratulations! Your certificate and chain have been saved at:{br}'
'{0}{br}{1}'
'Your cert will expire on {2}. To obtain a new or tweaked version of this '
'Your certificate will expire on {2}. To obtain a new or tweaked version of this '
'certificate in the future, simply run {3} again{4}. '
'To non-interactively renew *all* of your certificates, run "{3} renew"'
.format(fullchain_path, privkey_statement, expiry, cli.cli_command, verbswitch,
@ -596,8 +597,8 @@ def _delete_if_appropriate(config):
attempt_deletion = config.delete_after_revoke
if attempt_deletion is None:
msg = ("Would you like to delete the cert(s) you just revoked, along with all earlier and "
"later versions of the cert?")
msg = ("Would you like to delete the certificate(s) you just revoked, "
"along with all earlier and later versions of the certificate?")
attempt_deletion = display.yesno(msg, yes_label="Yes (recommended)", no_label="No",
force_interactive=True, default=True)
@ -619,8 +620,8 @@ def _delete_if_appropriate(config):
cert_manager.match_and_check_overlaps(config, [lambda x: archive_dir],
lambda x: x.archive_dir, lambda x: x)
except errors.OverlappingMatchFound:
logger.warning("Not deleting revoked certs due to overlapping archive dirs. More than "
"one certificate is using %s", archive_dir)
logger.warning("Not deleting revoked certificates due to overlapping archive dirs. "
"More than one certificate is using %s", archive_dir)
return
except Exception as e:
msg = ('config.default_archive_dir: {0}, config.live_dir: {1}, archive_dir: {2},'
@ -768,7 +769,7 @@ def update_account(config, unused_plugins):
acc.regr = acc.regr.update(uri=prev_regr_uri)
account_storage.update_regr(acc, cb_client.acme)
if config.email is None:
if not config.email:
display_util.notify("Any contact information associated "
"with this account has been removed.")
else:
@ -1097,7 +1098,7 @@ def revoke(config, unused_plugins):
raise errors.Error("Error! Exactly one of --cert-path or --cert-name must be specified!")
if config.key_path is not None: # revocation by cert key
logger.debug("Revoking %s using cert key %s",
logger.debug("Revoking %s using certificate key %s",
config.cert_path[0], config.key_path[0])
crypto_util.verify_cert_matches_priv_key(config.cert_path[0], config.key_path[0])
key = jose.JWK.load(config.key_path[1])
@ -1381,6 +1382,7 @@ def main(cli_args=None):
plugins = plugins_disco.PluginsRegistry.find_all()
logger.debug("certbot version: %s", certbot.__version__)
logger.debug("Location of certbot entry point: %s", sys.argv[0])
# 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)
@ -1402,6 +1404,13 @@ def main(cli_args=None):
if config.func != plugins_cmd: # pylint: disable=comparison-with-callable
raise
if sys.version_info[0] == 2:
warnings.warn(
"Python 2 support will be dropped in the next release of Certbot. "
"Please upgrade your Python version.",
PendingDeprecationWarning,
) # pragma: no cover
set_displayer(config)
# Reporter

View file

@ -1,7 +1,6 @@
"""Null plugin."""
import logging
import zope.component
import zope.interface
from certbot import interfaces

View file

@ -19,6 +19,7 @@ import zope.component
from acme.magic_typing import List
from acme.magic_typing import Optional # pylint: disable=unused-import
from certbot import crypto_util
from certbot.display import util as display_util
from certbot import errors
from certbot import interfaces
from certbot import util
@ -98,7 +99,7 @@ def _reconstitute(config, full_path):
config.domains = [util.enforce_domain_sanity(d)
for d in renewal_candidate.names()]
except errors.ConfigurationError as error:
logger.warning("Renewal configuration file %s references a cert "
logger.warning("Renewal configuration file %s references a certificate "
"that contains an invalid domain name. The problem "
"was: %s. Skipping.", full_path, error)
return None
@ -292,13 +293,13 @@ 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!"
# Some lineages may have begun with --staging, but then had production certs
# added to them
# Some lineages may have begun with --staging, but then had production
# certificates added to them
with open(lineage.cert) as the_file:
contents = the_file.read()
latest_cert = OpenSSL.crypto.load_certificate(
OpenSSL.crypto.FILETYPE_PEM, contents)
# all our test certs are from happy hacker fake CA, though maybe one day
# all our test certificates are from happy hacker fake CA, though maybe one day
# we should test more methodically
now_valid = "fake" not in repr(latest_cert.get_issuer()).lower()
@ -347,40 +348,42 @@ def report(msgs, category):
def _renew_describe_results(config, renew_successes, renew_failures,
renew_skipped, parse_failures):
# type: (interfaces.IConfig, List[str], List[str], List[str], List[str]) -> None
"""
Print a report to the terminal about the results of the renewal process.
out = [] # type: List[str]
notify = out.append
disp = zope.component.getUtility(interfaces.IDisplay)
:param interfaces.IConfig config: Configuration
:param list renew_successes: list of fullchain paths which were renewed
:param list renew_failures: list of fullchain paths which failed to be renewed
:param list renew_skipped: list of messages to print about skipped certificates
:param list parse_failures: list of renewal parameter paths which had erorrs
"""
notify = display_util.notify
notify_error = logger.error
def notify_error(err):
"""Notify and log errors."""
notify(str(err))
logger.error(err)
notify('\n{}'.format(display_util.SIDE_FRAME))
renewal_noun = "simulated renewal" if config.dry_run else "renewal"
if config.dry_run:
notify("** DRY RUN: simulating 'certbot renew' close to cert expiry")
notify("** (The test certificates below have not been saved.)")
notify("")
if renew_skipped:
notify("The following certs are not due for renewal yet:")
notify("The following certificates are not due for renewal yet:")
notify(report(renew_skipped, "skipped"))
if not renew_successes and not renew_failures:
notify("No renewals were attempted.")
notify("No {renewal}s were attempted.".format(renewal=renewal_noun))
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:")
notify("Congratulations, all {renewal}s succeeded: ".format(renewal=renewal_noun))
notify(report(renew_successes, "success"))
elif renew_failures and not renew_successes:
notify_error("All renewal attempts failed. The following certs could "
"not be renewed:")
notify_error("All %ss failed. The following certificates could "
"not be renewed:", renewal_noun)
notify_error(report(renew_failures, "failure"))
elif renew_failures and renew_successes:
notify("The following certs were successfully renewed:")
notify("The following {renewal}s succeeded:".format(renewal=renewal_noun))
notify(report(renew_successes, "success") + "\n")
notify_error("The following certs could not be renewed:")
notify_error("The following %ss failed:", renewal_noun)
notify_error(report(renew_failures, "failure"))
if parse_failures:
@ -388,11 +391,7 @@ def _renew_describe_results(config, renew_successes, renew_failures,
"were invalid: ")
notify(report(parse_failures, "parsefail"))
if config.dry_run:
notify("** DRY RUN: simulating 'certbot renew' close to cert expiry")
notify("** (The test certificates above have not been saved.)")
disp.notification("\n".join(out), wrap=False)
notify(display_util.SIDE_FRAME)
def handle_renewal_request(config):
@ -482,9 +481,10 @@ def handle_renewal_request(config):
except Exception as e: # pylint: disable=broad-except
# obtain_cert (presumably) encountered an unanticipated problem.
logger.warning("Attempting to renew cert (%s) from %s produced an "
"unexpected error: %s. Skipping.", lineagename,
renewal_file, e)
logger.error(
"Failed to renew certificate %s with error: %s",
lineagename, e
)
logger.debug("Traceback was:\n%s", traceback.format_exc())
renew_failures.append(renewal_candidate.fullchain)

View file

@ -810,8 +810,8 @@ class RenewableCert(interfaces.RenewableCert):
May need to recover from rare interrupted / crashed states."""
if self.has_pending_deployment():
logger.warning("Found a new cert /archive/ that was not linked to in /live/; "
"fixing...")
logger.warning("Found a new certificate /archive/ that was not "
"linked to in /live/; fixing...")
self.update_all_links_to(self.latest_common_version())
return False
return True
@ -884,7 +884,7 @@ class RenewableCert(interfaces.RenewableCert):
"""
target = self.current_target("cert")
if target is None:
raise errors.CertStorageError("could not find cert file")
raise errors.CertStorageError("could not find the certificate file")
with open(target) as f:
return crypto_util.get_names_from_cert(f.read())

View file

@ -279,7 +279,7 @@ def verify_renewable_cert_sig(renewable_cert):
verify_signed_payload(pk, cert.signature, cert.tbs_certificate_bytes,
cert.signature_hash_algorithm)
except (IOError, ValueError, InvalidSignature) as e:
error_str = "verifying the signature of the cert located at {0} has failed. \
error_str = "verifying the signature of the certificate located at {0} has failed. \
Details: {1}".format(renewable_cert.cert_path, e)
logger.exception(error_str)
raise errors.Error(error_str)
@ -330,7 +330,7 @@ def verify_cert_matches_priv_key(cert_path, key_path):
context.use_privatekey_file(key_path)
context.check_privatekey()
except (IOError, SSL.Error) as e:
error_str = "verifying the cert located at {0} matches the \
error_str = "verifying the certificate located at {0} matches the \
private key located at {1} has failed. \
Details: {2}".format(cert_path,
key_path, e)

View file

@ -167,7 +167,7 @@ def _determine_ocsp_server(cert_path):
if host:
return url, host
logger.info("Cannot process OCSP host from URL (%s) in cert at %s", url, cert_path)
logger.info("Cannot process OCSP host from URL (%s) in certificate at %s", url, cert_path)
return None, None
@ -222,7 +222,7 @@ def _check_ocsp_cryptography(cert_path, chain_path, url, timeout):
def _check_ocsp_response(response_ocsp, request_ocsp, issuer_cert, cert_path):
"""Verify that the OCSP is valid for serveral criteria"""
"""Verify that the OCSP is valid for several criteria"""
# Assert OCSP response corresponds to the certificate we are talking about
if response_ocsp.serial_number != request_ocsp.serial_number:
raise AssertionError('the certificate in response does not correspond '

View file

@ -439,7 +439,7 @@ def safe_email(email):
return False
class _ShowWarning(argparse.Action):
class DeprecatedArgumentAction(argparse.Action):
"""Action to log a warning when an argument is used."""
def __call__(self, unused1, unused2, unused3, option_string=None):
logger.warning("Use of %s is deprecated.", option_string)
@ -458,16 +458,16 @@ def add_deprecated_argument(add_argument, argument_name, nargs):
:param nargs: Value for nargs when adding the argument to argparse.
"""
if _ShowWarning not in configargparse.ACTION_TYPES_THAT_DONT_NEED_A_VALUE:
if DeprecatedArgumentAction not in configargparse.ACTION_TYPES_THAT_DONT_NEED_A_VALUE:
# In version 0.12.0 ACTION_TYPES_THAT_DONT_NEED_A_VALUE was
# changed from a set to a tuple.
if isinstance(configargparse.ACTION_TYPES_THAT_DONT_NEED_A_VALUE, set):
configargparse.ACTION_TYPES_THAT_DONT_NEED_A_VALUE.add(
_ShowWarning)
DeprecatedArgumentAction)
else:
configargparse.ACTION_TYPES_THAT_DONT_NEED_A_VALUE += (
_ShowWarning,)
add_argument(argument_name, action=_ShowWarning,
DeprecatedArgumentAction,)
add_argument(argument_name, action=DeprecatedArgumentAction,
help=argparse.SUPPRESS, nargs=nargs)

View file

@ -1,4 +1,4 @@
usage:
usage:
certbot [SUBCOMMAND] [options] [-d DOMAIN] [-d DOMAIN] ...
Certbot can obtain and install HTTPS/TLS/SSL certificates. By default,
@ -118,12 +118,12 @@ optional arguments:
case, and to know when to deprecate support for past
Python versions and flags. If you wish to hide this
information from the Let's Encrypt server, set this to
"". (default: CertbotACMEClient/1.9.0 (certbot(-auto);
OS_NAME OS_VERSION) Authenticator/XXX Installer/YYY
(SUBCOMMAND; flags: FLAGS) Py/major.minor.patchlevel).
The flags encoded in the user agent are: --duplicate,
--force-renew, --allow-subset-of-names, -n, and
whether any hooks are set.
"". (default: CertbotACMEClient/1.10.1
(certbot(-auto); OS_NAME OS_VERSION) Authenticator/XXX
Installer/YYY (SUBCOMMAND; flags: FLAGS)
Py/major.minor.patchlevel). The flags encoded in the
user agent are: --duplicate, --force-renew, --allow-
subset-of-names, -n, and whether any hooks are set.
--user-agent-comment USER_AGENT_COMMENT
Add a comment to the default user agent string. May be
used when repackaging Certbot or calling it from
@ -188,12 +188,12 @@ security:
Security parameters & server settings
--rsa-key-size N Size of the RSA key. (default: 2048)
--key-type type The type of algorithm to use for the the private key.
Either ``rsa`` or ``ecdsa``. (default: ``rsa``).
--elliptic-curve The elliptic curve to use when choosing ``ecdsa`` as the key
type. Accepted values are SECG curve names as defined by
the cryptography library. ``secp256r1``, ``secp384r1``,
``secp521r1``. (default: secp256r1).
--key-type {rsa,ecdsa}
Type of generated private key(Only *ONE* per
invocation can be provided at this time) (default:
rsa)
--elliptic-curve N The SECG elliptic curve name to use. Please see RFC
8446 for supported values. (default: secp256r1)
--must-staple Adds the OCSP Must Staple extension to the
certificate. Autoconfigures OCSP Stapling for
supported setups (Apache version >= 2.3.3 ). (default:
@ -694,8 +694,6 @@ manual:
--manual-cleanup-hook MANUAL_CLEANUP_HOOK
Path or command to execute for the cleanup script
(default: None)
--manual-public-ip-logging-ok
Automatically allows public IP logging (default: Ask)
nginx:
Nginx Web Server plugin

View file

@ -9,7 +9,7 @@ application itself. This means that we will not change behavior in a backwards
incompatible way except in a new major version of the project.
.. note:: None of this applies to the behavior of Certbot distribution
mechanisms such as :ref:`certbot-auto <certbot-auto>` or OS packages whose
mechanisms such as :ref:`our snaps <snap-install>` or OS packages whose
behavior may change at any time. Semantic versioning only applies to the
common Certbot components that are installed by various distribution
methods.

View file

@ -282,8 +282,8 @@ support for IIS, Icecast and Plesk.
Installers and Authenticators will oftentimes be the same class/object
(because for instance both tasks can be performed by a webserver like nginx)
though this is not always the case (the standalone plugin is an authenticator
that listens on port 80, but it cannot install certs; a postfix plugin would
be an installer but not an authenticator).
that listens on port 80, but it cannot install certificates; a postfix plugin
would be an installer but not an authenticator).
Installers and Authenticators are kept separate because
it should be possible to use the `~.StandaloneAuthenticator` (it sets
@ -516,11 +516,13 @@ Steps:
4. Run ``tox --skip-missing-interpreters`` to run the entire test suite
including coverage. The ``--skip-missing-interpreters`` argument ignores
missing versions of Python needed for running the tests. Fix any errors.
5. Submit the PR. Once your PR is open, please do not force push to the branch
5. If any documentation should be added or updated as part of the changes you
have made, please include the documentation changes in your PR.
6. Submit the PR. Once your PR is open, please do not force push to the branch
containing your pull request to squash or amend commits. We use `squash
merges <https://github.com/blog/2141-squash-your-commits>`_ on PRs and
rewriting commits makes changes harder to track between reviews.
6. Did your tests pass on Azure Pipelines? If they didn't, fix any errors.
7. Did your tests pass on Azure Pipelines? If they didn't, fix any errors.
.. _ask for help:

View file

@ -44,17 +44,6 @@ supports
<https://github.com/certbot/certbot/blob/master/certbot-apache/certbot_apache/_internal/constants.py>`_
modern OSes based on Debian, Ubuntu, Fedora, SUSE, Gentoo and Darwin.
Additional integrity verification of certbot-auto script can be done by verifying its digital signature.
This requires a local installation of gpg2, which comes packaged in many Linux distributions under name gnupg or gnupg2.
Installing with ``certbot-auto`` requires 512MB of RAM in order to build some
of the dependencies. Installing from pre-built OS packages avoids this
requirement. You can also temporarily set a swap file. See "Problems with
Python virtual environment" below for details.
Alternate installation methods
================================
@ -78,74 +67,6 @@ choosing "snapd" in the "System" dropdown menu. (You should select "snapd"
regardless of your operating system, as our instructions are the same across
all systems.)
.. _certbot-auto:
Certbot-Auto
------------
The ``certbot-auto`` wrapper script installs Certbot, obtaining some dependencies
from your web server OS and putting others in a python virtual environment. You can
download and run it as follows::
wget https://dl.eff.org/certbot-auto
sudo mv certbot-auto /usr/local/bin/certbot-auto
sudo chown root /usr/local/bin/certbot-auto
sudo chmod 0755 /usr/local/bin/certbot-auto
/usr/local/bin/certbot-auto --help
To remove certbot-auto, just delete it and the files it places under /opt/eff.org, along with any cronjob or systemd timer you may have created.
To check the integrity of the ``certbot-auto`` script,
you can use these steps::
user@webserver:~$ wget -N https://dl.eff.org/certbot-auto.asc
user@webserver:~$ gpg2 --keyserver pool.sks-keyservers.net --recv-key A2CFB51FA275A7286234E7B24D17C995CD9775F2
user@webserver:~$ gpg2 --trusted-key 4D17C995CD9775F2 --verify certbot-auto.asc /usr/local/bin/certbot-auto
The output of the last command should look something like::
gpg: Signature made Wed 02 May 2018 05:29:12 AM IST
gpg: using RSA key A2CFB51FA275A7286234E7B24D17C995CD9775F2
gpg: key 4D17C995CD9775F2 marked as ultimately trusted
gpg: checking the trustdb
gpg: marginals needed: 3 completes needed: 1 trust model: pgp
gpg: depth: 0 valid: 2 signed: 2 trust: 0-, 0q, 0n, 0m, 0f, 2u
gpg: depth: 1 valid: 2 signed: 0 trust: 2-, 0q, 0n, 0m, 0f, 0u
gpg: next trustdb check due at 2027-11-22
gpg: Good signature from "Let's Encrypt Client Team <letsencrypt-client@eff.org>" [ultimate]
The ``certbot-auto`` command updates to the latest client release automatically.
Since ``certbot-auto`` is a wrapper to ``certbot``, it accepts exactly
the same command line flags and arguments. For more information, see
`Certbot command-line options <https://certbot.eff.org/docs/using.html#command-line-options>`_.
For full command line help, you can type::
/usr/local/bin/certbot-auto --help all
Problems with Python virtual environment
----------------------------------------
On a low memory system such as VPS with less than 512MB of RAM, the required dependencies of Certbot will fail to build.
This can be identified if the pip outputs contains something like ``internal compiler error: Killed (program cc1)``.
You can workaround this restriction by creating a temporary swapfile::
user@webserver:~$ sudo fallocate -l 1G /tmp/swapfile
user@webserver:~$ sudo chmod 600 /tmp/swapfile
user@webserver:~$ sudo mkswap /tmp/swapfile
user@webserver:~$ sudo swapon /tmp/swapfile
Disable and remove the swapfile once the virtual environment is constructed::
user@webserver:~$ sudo swapoff /tmp/swapfile
user@webserver:~$ sudo rm /tmp/swapfile
.. _docker-user:
Running with Docker
@ -161,7 +82,7 @@ Docker if you are sure you know what you are doing and have a good reason to do
so.
You should definitely read the :ref:`where-certs` section, in order to
know how to manage the certs
know how to manage the certificates
manually. `Our ciphersuites page <ciphers.html>`__
provides some information about recommended ciphersuites. If none of
these make much sense to you, you should definitely use the installation method
@ -207,6 +128,18 @@ of the ``/etc/letsencrypt`` directory, see :ref:`where-certs`.
Operating System Packages
-------------------------
.. warning:: While the Certbot team tries to keep the Certbot packages offered
by various operating systems working in the most basic sense, due to
distribution policies and/or the limited resources of distribution
maintainers, Certbot OS packages often have problems that other distribution
mechanisms do not. The packages are often old resulting in a lack of bug
fixes and features and a worse TLS configuration than is generated by newer
versions of Certbot. They also may not configure certificate renewal for you
or have all of Certbot's plugins available. For reasons like these, we
recommend most users follow the instructions at
https://certbot.eff.org/instructions and OS packages are only documented
here as an alternative.
**Arch Linux**
.. code-block:: shell
@ -273,8 +206,8 @@ Optionally to install the Certbot Apache plugin, you can use:
**Gentoo**
The official Certbot client is available in Gentoo Portage. From the
official Certbot plugins, three of them are also available in Portage.
The official Certbot client is available in Gentoo Portage. From the
official Certbot plugins, three of them are also available in Portage.
They need to be installed separately if you require their functionality.
.. code-block:: shell
@ -284,7 +217,7 @@ They need to be installed separately if you require their functionality.
emerge -av app-crypt/certbot-nginx
emerge -av app-crypt/certbot-dns-nsone
.. Note:: The ``app-crypt/certbot-dns-nsone`` package has a different
.. Note:: The ``app-crypt/certbot-dns-nsone`` package has a different
maintainer than the other packages and can lag behind in version.
**NetBSD**
@ -303,6 +236,35 @@ OS packaging is an ongoing effort. If you'd like to package
Certbot for your distribution of choice please have a
look at the :doc:`packaging`.
.. _certbot-auto:
Certbot-Auto
------------
We used to have a shell script named ``certbot-auto`` to help people install
Certbot on UNIX operating systems, however, this script is no longer supported.
If you want to uninstall ``certbot-auto``, you can follow our instructions
:doc:`here <uninstall>`.
Problems with Python virtual environment
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
When using ``certbot-auto`` on a low memory system such as VPS with less than
512MB of RAM, the required dependencies of Certbot may fail to build. This can
be identified if the pip outputs contains something like ``internal compiler
error: Killed (program cc1)``. You can workaround this restriction by creating
a temporary swapfile::
user@webserver:~$ sudo fallocate -l 1G /tmp/swapfile
user@webserver:~$ sudo chmod 600 /tmp/swapfile
user@webserver:~$ sudo mkswap /tmp/swapfile
user@webserver:~$ sudo swapon /tmp/swapfile
Disable and remove the swapfile once the virtual environment is constructed::
user@webserver:~$ sudo swapoff /tmp/swapfile
user@webserver:~$ sudo rm /tmp/swapfile
Installing from source
----------------------

View file

@ -0,0 +1,16 @@
=========================
Uninstalling certbot-auto
=========================
To uninstall ``certbot-auto``, you need to do three things:
1. If you added a cron job or systemd timer to automatically run
``certbot-auto`` to renew your certificates, you should delete it. If you
did this by following our instructions, you can delete the entry added to
``/etc/crontab`` by running a command like ``sudo sed -i '/certbot-auto/d'
/etc/crontab``.
2. Delete the ``certbot-auto`` script. If you placed it in ``/usr/local/bin``
like we recommended, you can delete it by running ``sudo rm
/usr/local/bin/certbot-auto``.
3. Delete the Certbot installation created by ``certbot-auto`` by running
``sudo rm -rf /opt/eff.org``.

View file

@ -179,10 +179,9 @@ If you'd like to obtain a wildcard certificate from Let's Encrypt or run
Certbot's DNS plugins.
These plugins are not included in a default Certbot installation and must be
installed separately. While the DNS plugins cannot currently be used with
``certbot-auto``, they are available in many OS package managers, as Docker
images, and as snaps. Visit https://certbot.eff.org to learn the best way to use
the DNS plugins on your system.
installed separately. They are available in many OS package managers, as Docker
images, and as snaps. Visit https://certbot.eff.org to learn the best way to
use the DNS plugins on your system.
Once installed, you can find documentation on how to use each plugin at:
@ -314,7 +313,7 @@ the ``certificates`` subcommand:
This returns information in the following format::
Found the following certs:
Found the following certificates:
Certificate Name: example.com
Domains: example.com, www.example.com
Expiry Date: 2017-02-19 19:53:00+00:00 (VALID: 30 days)
@ -913,7 +912,7 @@ Changing the ACME Server
========================
By default, Certbot uses Let's Encrypt's production server at
https://acme-v02.api.letsencrypt.org/. You can tell Certbot to use a
https://acme-v02.api.letsencrypt.org/directory. You can tell Certbot to use a
different CA by providing ``--server`` on the command line or in a
:ref:`configuration file <config-file>` with the URL of the server's
ACME directory. For example, if you would like to use Let's Encrypt's

View file

@ -13,7 +13,6 @@ except ImportError: # pragma: no cover
from certbot import errors
from certbot import util
from certbot._internal import account
from certbot.compat import filesystem
from certbot.compat import os
import certbot.tests.util as test_util

View file

@ -1,7 +1,6 @@
"""Tests for certbot.compat.filesystem"""
import contextlib
import errno
import stat
import unittest
try:

View file

@ -1,6 +1,11 @@
"""Tests for certbot.helpful_parser"""
import unittest
try:
import mock
except ImportError: # pragma: no cover
from unittest import mock
from certbot import errors
from certbot._internal.cli import HelpfulArgumentParser
from certbot._internal.cli import _DomainsAction
@ -189,5 +194,16 @@ class TestParseArgsErrors(unittest.TestCase):
arg_parser.parse_args()
class TestAddDeprecatedArgument(unittest.TestCase):
"""Tests for add_deprecated_argument method of HelpfulArgumentParser"""
@mock.patch.object(HelpfulArgumentParser, "modify_kwargs_for_default_detection")
def test_no_default_detection_modifications(self, mock_modify):
arg_parser = HelpfulArgumentParser(["run"], {}, detect_defaults=True)
arg_parser.add_deprecated_argument("--foo", 0)
arg_parser.parse_args()
mock_modify.assert_not_called()
if __name__ == '__main__':
unittest.main() # pragma: no cover

View file

@ -1169,7 +1169,7 @@ class MainTest(test_util.ConfigTestCase):
_, _, stdout = self._test_renewal_common(False, extra_args=None, should_renew=False,
args=['renew'], expiry_date=expiry)
self.assertTrue('No renewals were attempted.' in stdout.getvalue())
self.assertTrue('The following certs are not due for renewal yet:' in stdout.getvalue())
self.assertTrue('The following certificates are not due for renewal yet:' in stdout.getvalue())
@mock.patch('certbot._internal.log.post_arg_parse_setup')
def test_quiet_renew(self, _):
@ -1444,85 +1444,6 @@ class MainTest(test_util.ConfigTestCase):
x = self._call_no_clientmock(["register", "--email", "user@example.org"])
self.assertTrue("There is an existing account" in x[0])
def test_update_account_no_existing_accounts(self):
# with mock.patch('certbot._internal.main.client') as mocked_client:
with mock.patch('certbot._internal.main.account') as mocked_account:
mocked_storage = mock.MagicMock()
mocked_account.AccountFileStorage.return_value = mocked_storage
mocked_storage.find_all.return_value = []
x = self._call_no_clientmock(
["update_account", "--email",
"user@example.org"])
self.assertTrue("Could not find an existing account" in x[0])
@mock.patch('certbot._internal.main._determine_account')
@mock.patch('certbot._internal.eff.prepare_subscription')
@mock.patch('certbot._internal.main.account')
def test_update_account_remove_email(self, mocked_account_module, mock_prepare, mock_det_acc):
# Mock account storage and the account object returned
mocked_storage = mock.MagicMock()
mocked_account = mock.MagicMock()
mocked_account_module.AccountFileStorage.return_value = mocked_storage
mocked_storage.find_all.return_value = [mocked_account]
mock_det_acc.return_value = (mocked_account, "foo")
# Mock registration body to verify calls are made
mock_regr_body = mock.MagicMock()
# mocked_account.regr is overwritten in update, requiring an odd mock setup
mocked_account.regr.body = mock_regr_body
x = self._call(
["update_account", "--register-unsafely-without-email"])
# When update succeeds, the return value of update_account() is None
self.assertTrue(x[0] is None)
# and we got supposedly did update the registration from
# the server
client_mock = x[3]
self.assertTrue(client_mock.Client().acme.update_registration.called)
self.assertTrue(mock_regr_body.update.called)
self.assertTrue('contact' in mock_regr_body.update.call_args[1])
self.assertEqual(mock_regr_body.update.call_args[1]['contact'], ())
# and we saved the updated registration on disk
self.assertTrue(mocked_storage.update_regr.called)
# ensure we didn't try to subscribe (no email to subscribe with)
self.assertFalse(mock_prepare.called)
@mock.patch("certbot._internal.main.display_util.notify")
@mock.patch('certbot._internal.main.display_ops.get_email')
@test_util.patch_get_utility()
def test_update_account_with_email(self, mock_utility, mock_email, mock_notify):
email = "user@example.com"
mock_email.return_value = email
with mock.patch('certbot._internal.eff.prepare_subscription') as mock_prepare:
with mock.patch('certbot._internal.main._determine_account') as mocked_det:
with mock.patch('certbot._internal.main.account') as mocked_account:
with mock.patch('certbot._internal.main.client') as mocked_client:
mocked_storage = mock.MagicMock()
mocked_account.AccountFileStorage.return_value = mocked_storage
mocked_storage.find_all.return_value = ["an account"]
mocked_det.return_value = (mock.MagicMock(), "foo")
cb_client = mock.MagicMock()
mocked_client.Client.return_value = cb_client
x = self._call_no_clientmock(
["update_account"])
# When registration change succeeds, the return value
# of register() is None
self.assertTrue(x[0] is None)
# and we got supposedly did update the registration from
# the server
self.assertTrue(
cb_client.acme.update_registration.called)
# and we saved the updated registration on disk
self.assertTrue(mocked_storage.update_regr.called)
self.assertTrue(
email in mock_notify.call_args[0][0])
self.assertTrue(mock_prepare.called)
@mock.patch('certbot._internal.plugins.selection.choose_configurator_plugins')
@mock.patch('certbot._internal.updater._run_updaters')
def test_plugin_selection_error(self, mock_run, mock_choose):
@ -1795,5 +1716,111 @@ class InstallTest(test_util.ConfigTestCase):
self.config, plugins)
class UpdateAccountTest(test_util.ConfigTestCase):
"""Tests for certbot._internal.main.update_account"""
def setUp(self):
patches = {
'account': mock.patch('certbot._internal.main.account'),
'atexit': mock.patch('certbot.util.atexit'),
'client': mock.patch('certbot._internal.main.client'),
'determine_account': mock.patch('certbot._internal.main._determine_account'),
'notify': mock.patch('certbot._internal.main.display_util.notify'),
'prepare_sub': mock.patch('certbot._internal.eff.prepare_subscription'),
'util': test_util.patch_get_utility()
}
self.mocks = { k: patches[k].start() for k in patches }
for patch in patches.values():
self.addCleanup(patch.stop)
return super(UpdateAccountTest, self).setUp()
def _call(self, args):
with mock.patch('certbot._internal.main.sys.stdout'), \
mock.patch('certbot._internal.main.sys.stderr'):
args = ['--config-dir', self.config.config_dir,
'--work-dir', self.config.work_dir,
'--logs-dir', self.config.logs_dir, '--text'] + args
return main.main(args[:]) # NOTE: parser can alter its args!
def _prepare_mock_account(self):
mock_storage = mock.MagicMock()
mock_account = mock.MagicMock()
mock_regr = mock.MagicMock()
mock_storage.find_all.return_value = [mock_account]
self.mocks['account'].AccountFileStorage.return_value = mock_storage
mock_account.regr.body = mock_regr.body
self.mocks['determine_account'].return_value = (mock_account, mock.MagicMock())
return (mock_account, mock_storage, mock_regr)
def _test_update_no_contact(self, args):
"""Utility to assert that email removal is handled correctly"""
(_, mock_storage, mock_regr) = self._prepare_mock_account()
result = self._call(args)
# When update succeeds, the return value of update_account() is None
self.assertIsNone(result)
# We submitted a registration to the server
self.assertEqual(self.mocks['client'].Client().acme.update_registration.call_count, 1)
mock_regr.body.update.assert_called_with(contact=())
# We got an update from the server and persisted it
self.assertEqual(mock_storage.update_regr.call_count, 1)
# We should have notified the user
self.mocks['notify'].assert_called_with(
'Any contact information associated with this account has been removed.'
)
# We should not have called subscription because there's no email
self.mocks['prepare_sub'].assert_not_called()
def test_no_existing_accounts(self):
"""Test that no existing account is handled correctly"""
mock_storage = mock.MagicMock()
mock_storage.find_all.return_value = []
self.mocks['account'].AccountFileStorage.return_value = mock_storage
self.assertEqual(self._call(['update_account', '--email', 'user@example.org']),
'Could not find an existing account to update.')
def test_update_account_remove_email(self):
"""Test that --register-unsafely-without-email is handled as no email"""
self._test_update_no_contact(['update_account', '--register-unsafely-without-email'])
def test_update_account_empty_email(self):
"""Test that providing an empty email is handled as no email"""
self._test_update_no_contact(['update_account', '-m', ''])
@mock.patch('certbot._internal.main.display_ops.get_email')
def test_update_account_with_email(self, mock_email):
"""Test that updating with a singular email is handled correctly"""
mock_email.return_value = 'user@example.com'
(_, mock_storage, _) = self._prepare_mock_account()
mock_client = mock.MagicMock()
self.mocks['client'].Client.return_value = mock_client
result = self._call(['update_account'])
# None if registration succeeds
self.assertIsNone(result)
# We should have updated the server
self.assertEqual(mock_client.acme.update_registration.call_count, 1)
# We should have updated the account on disk
self.assertEqual(mock_storage.update_regr.call_count, 1)
# Subscription should have been prompted
self.assertEqual(self.mocks['prepare_sub'].call_count, 1)
# Should have printed the email
self.mocks['notify'].assert_called_with(
'Your e-mail address was updated to user@example.com.')
def test_update_account_with_multiple_emails(self):
"""Test that multiple email addresses are handled correctly"""
(_, mock_storage, mock_regr) = self._prepare_mock_account()
self.assertIsNone(
self._call(['update_account', '-m', 'user@example.com,user@example.org'])
)
mock_regr.body.update.assert_called_with(
contact=['mailto:user@example.com', 'mailto:user@example.org']
)
self.assertEqual(mock_storage.update_regr.call_count, 1)
self.mocks['notify'].assert_called_with(
'Your e-mail address was updated to user@example.com,user@example.org.')
if __name__ == '__main__':
unittest.main() # pragma: no cover

View file

@ -163,5 +163,70 @@ class RestoreRequiredConfigElementsTest(test_util.ConfigTestCase):
self.assertEqual(self.config.server, constants.CLI_DEFAULTS['server'])
class DescribeResultsTest(unittest.TestCase):
"""Tests for certbot._internal.renewal._renew_describe_results."""
def setUp(self):
self.patchers = {
'log_error': mock.patch('certbot._internal.renewal.logger.error'),
'notify': mock.patch('certbot._internal.renewal.display_util.notify')}
self.mock_notify = self.patchers['notify'].start()
self.mock_error = self.patchers['log_error'].start()
def tearDown(self):
for patch in self.patchers.values():
patch.stop()
@classmethod
def _call(cls, *args, **kwargs):
from certbot._internal.renewal import _renew_describe_results
_renew_describe_results(*args, **kwargs)
def _assert_success_output(self, lines):
self.mock_notify.assert_has_calls([mock.call(l) for l in lines])
def test_no_renewal_attempts(self):
self._call(mock.MagicMock(dry_run=True), [], [], [], [])
self._assert_success_output(['No simulated renewals were attempted.'])
def test_successful_renewal(self):
self._call(mock.MagicMock(dry_run=False), ['good.pem'], None, None, None)
self._assert_success_output([
'\n- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -',
'Congratulations, all renewals succeeded: ',
' good.pem (success)',
'- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -',
])
def test_failed_renewal(self):
self._call(mock.MagicMock(dry_run=False), [], ['bad.pem'], [], [])
self._assert_success_output([
'\n- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -',
'- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -',
])
self.mock_error.assert_has_calls([
mock.call('All %ss failed. The following certificates could not be renewed:', 'renewal'),
mock.call(' bad.pem (failure)'),
])
def test_all_renewal(self):
self._call(mock.MagicMock(dry_run=True),
['good.pem', 'good2.pem'], ['bad.pem', 'bad2.pem'],
['foo.pem expires on 123'], ['errored.conf'])
self._assert_success_output([
'\n- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -',
'The following certificates are not due for renewal yet:',
' foo.pem expires on 123 (skipped)',
'The following simulated renewals succeeded:',
' good.pem (success)\n good2.pem (success)\n',
'\nAdditionally, the following renewal configurations were invalid: ',
' errored.conf (parsefail)',
'- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -',
])
self.mock_error.assert_has_calls([
mock.call('The following %ss failed:', 'simulated renewal'),
mock.call(' bad.pem (failure)\n bad2.pem (failure)'),
])
if __name__ == "__main__":
unittest.main() # pragma: no cover

View file

@ -547,8 +547,7 @@ class OsInfoTest(unittest.TestCase):
m_distro.linux_distribution.return_value = ("something", "else")
self.assertEqual(cbutil.get_os_info(), ("something", "else"))
@mock.patch("certbot.util.subprocess.Popen")
def test_non_systemd_os_info(self, popen_mock):
def test_non_systemd_os_info(self):
import certbot.util as cbutil
with mock.patch('certbot.util._USE_DISTRO', False):
with mock.patch('platform.system_alias',
@ -557,13 +556,14 @@ class OsInfoTest(unittest.TestCase):
with mock.patch('platform.system_alias',
return_value=('darwin', '', '')):
comm_mock = mock.Mock()
comm_attrs = {'communicate.return_value':
('42.42.42', 'error')}
comm_mock.configure_mock(**comm_attrs)
popen_mock.return_value = comm_mock
self.assertEqual(cbutil.get_python_os_info()[0], 'darwin')
self.assertEqual(cbutil.get_python_os_info()[1], '42.42.42')
with mock.patch("subprocess.Popen") as popen_mock:
comm_mock = mock.Mock()
comm_attrs = {'communicate.return_value':
('42.42.42', 'error')}
comm_mock.configure_mock(**comm_attrs)
popen_mock.return_value = comm_mock
self.assertEqual(cbutil.get_python_os_info()[0], 'darwin')
self.assertEqual(cbutil.get_python_os_info()[1], '42.42.42')
with mock.patch('platform.system_alias',
return_value=('freebsd', '9.3-RC3-p1', '')):

View file

@ -31,7 +31,7 @@ if [ -z "$VENV_PATH" ]; then
fi
VENV_BIN="$VENV_PATH/bin"
BOOTSTRAP_VERSION_PATH="$VENV_PATH/certbot-auto-bootstrap-version.txt"
LE_AUTO_VERSION="1.9.0"
LE_AUTO_VERSION="1.10.1"
BASENAME=$(basename $0)
USAGE="Usage: $BASENAME [OPTIONS]
A self-updating wrapper script for the Certbot ACME client. When run, updates
@ -799,11 +799,7 @@ BootstrapMageiaCommon() {
# that function. If Bootstrap is set to a function that doesn't install any
# packages BOOTSTRAP_VERSION is not set.
if [ -f /etc/debian_version ]; then
Bootstrap() {
BootstrapMessage "Debian-based OSes"
BootstrapDebCommon
}
BOOTSTRAP_VERSION="BootstrapDebCommon $BOOTSTRAP_DEB_COMMON_VERSION"
DEPRECATED_OS=1
elif [ -f /etc/mageia-release ]; then
# Mageia has both /etc/mageia-release and /etc/redhat-release
DEPRECATED_OS=1
@ -1497,18 +1493,18 @@ letsencrypt==0.7.0 \
--hash=sha256:105a5fb107e45bcd0722eb89696986dcf5f08a86a321d6aef25a0c7c63375ade \
--hash=sha256:c36e532c486a7e92155ee09da54b436a3c420813ec1c590b98f635d924720de9
certbot==1.9.0 \
--hash=sha256:d5a804d32e471050921f7b39ed9859e2e9de02824176ed78f57266222036b53a \
--hash=sha256:2ff9bf7d9af381c7efee22dec2dd6938d9d8fddcc9e11682b86e734164a30b57
acme==1.9.0 \
--hash=sha256:d8061b396a22b21782c9b23ff9a945b23e50fca2573909a42f845e11d5658ac5 \
--hash=sha256:38a1630c98e144136c62eec4d2c545a1bdb1a3cd4eca82214be6b83a1f5a161f
certbot-apache==1.9.0 \
--hash=sha256:09528a820d57e54984d490100644cd8a6603db97bf5776f86e95795ecfacf23d \
--hash=sha256:f47fb3f4a9bd927f4812121a0beefe56b163475a28f4db34c64dc838688d9e9e
certbot-nginx==1.9.0 \
--hash=sha256:bb2e3f7fe17f071f350a3efa48571b8ef40a8e4b6db9c6da72539206a20b70be \
--hash=sha256:ab26a4f49d53b0e8bf0f903e58e2a840cda233fe1cbbc54c36ff17f973e57d65
certbot==1.10.1 \
--hash=sha256:011ac980fa21b9f29e02c9b8d8b86e8a4bf4670b51b6ad91656e401e9d2d2231 \
--hash=sha256:0d9ee3fc09e0d03b2d1b1f1c4916e61ecfc6904b4216ddef4e6a5ca1424d9cb7
acme==1.10.1 \
--hash=sha256:752d598e54e98ad1e874de53fd50c61044f1b566d6deb790db5676ce9c573546 \
--hash=sha256:fcbb559aedc96b404edf593e78517dcd7291984d5a37036c3fc77f3c5c122fd8
certbot-apache==1.10.1 \
--hash=sha256:f077b4b7f166627ef5e0921fe7cde57700670fc86e9ad9dbdfaf2c573cc0f2fa \
--hash=sha256:97ed637b4c7b03820db6c69aa90145dc989933351d46a3d62baf6b71674f0a10
certbot-nginx==1.10.1 \
--hash=sha256:7c36459021f8a1ec3b6c062e4c4fc866bfaa1dbf26ccd29e043dd6848003be08 \
--hash=sha256:c0bbeccf85f46b728fd95e6bb8c2649d32d3383d7f47ea4b9c312d12bf04d2f0
UNLIKELY_EOF
# -------------------------------------------------------------------------

View file

@ -1,54 +0,0 @@
# For running tests, build a docker image with a passwordless sudo and a trust
# store we can manipulate.
ARG REDHAT_DIST_FLAVOR
FROM ${REDHAT_DIST_FLAVOR}:6
ARG REDHAT_DIST_FLAVOR
RUN curl -O https://dl.fedoraproject.org/pub/epel/epel-release-latest-6.noarch.rpm \
&& rpm -ivh epel-release-latest-6.noarch.rpm
# Install pip and sudo:
RUN yum install -y python-pip sudo
# Update to a stable and tested version of pip.
# We do not use pipstrap here because it no longer supports Python 2.6.
RUN pip install pip==9.0.1 setuptools==29.0.1 wheel==0.29.0
# Pin pytest version for increased stability
RUN pip install pytest==3.2.5 six==1.10.0
# Add an unprivileged user:
RUN useradd --create-home --home-dir /home/lea --shell /bin/bash --groups wheel --uid 1000 lea
# Let that user sudo:
RUN sed -i.bkp -e \
's/# %wheel\(NOPASSWD: ALL\)\?/%wheel/g' \
/etc/sudoers
RUN mkdir -p /home/lea/certbot
# Install fake testing CA:
COPY ./tests/certs/ca/my-root-ca.crt.pem /usr/local/share/ca-certificates/
RUN update-ca-trust
# Copy current letsencrypt-auto:
COPY . /home/lea/certbot/letsencrypt-auto-source
# Tweak uname binary for tests on fake 32bits
COPY tests/uname_wrapper.sh /bin
RUN mv /bin/uname /bin/uname_orig \
&& mv /bin/uname_wrapper.sh /bin/uname \
&& chmod +x /bin/uname
# Fetch previous letsencrypt-auto that was installing python 3.4
RUN curl https://raw.githubusercontent.com/certbot/certbot/v0.38.0/letsencrypt-auto-source/letsencrypt-auto \
-o /home/lea/certbot/letsencrypt-auto-source/letsencrypt-auto_py_34 \
&& chmod +x /home/lea/certbot/letsencrypt-auto-source/letsencrypt-auto_py_34
RUN cp /home/lea/certbot/letsencrypt-auto-source/tests/${REDHAT_DIST_FLAVOR}6_tests.sh /home/lea/certbot/letsencrypt-auto-source/tests/redhat6_tests.sh \
&& chmod +x /home/lea/certbot/letsencrypt-auto-source/tests/redhat6_tests.sh
USER lea
WORKDIR /home/lea
CMD ["sudo", "certbot/letsencrypt-auto-source/tests/redhat6_tests.sh"]

View file

@ -1,11 +1,11 @@
-----BEGIN PGP SIGNATURE-----
iQEzBAABCAAdFiEEos+1H6J1pyhiNOeyTRfJlc2XdfIFAl98wk8ACgkQTRfJlc2X
dfIctgf/TO83xXJJ8haqxke0ehHCwcmipX7ijPhwvaUTSqciMa56KnGJLNp1lAVz
vv8sfHUf7NSvGlRg+5M0szWY25+JzveJDNzse3rOzFmxA1GNKUycE3/zE/IdBRwN
fmxJHaUBrBL2erBZPHe8gFGTvlzopBoGSmQpWGY3hIufPWKBJohCbTscKbaa9hyz
njmMvwRdeqzvLWVZ4jNDDsil9kKl2Emue3guzA/cvVxHe17DZyLDfqni7ysZIcTn
wPAQzpLBKHyiqVRoVk+BJ6Z6wamW4NAxKbjXy9GrHy4txlfW8tGd3jXha8yWqJeH
xEFK02Zp+T17+C5uqEW4o0cIofMjCw==
=9UGf
iQEzBAABCAAdFiEEos+1H6J1pyhiNOeyTRfJlc2XdfIFAl/JL3kACgkQTRfJlc2X
dfKJMwf/RXjfg5KScEjWiR+YMAcTVxGl4ITDMNBvmPoqCfrPwIJQewy1k6yQUITr
tMe0tkPneGgGccJreLAuO4+RdmNqm2MKBO3wMW9YZobJxcbMmrtVxyBD2OP4K/lL
oCZvjcN5pLvje6OlMwJ/fQ+zGY8mFUpfKIluxKrqkkO3p6Q+i/wPXF5Gjjb2J/bI
N+TczQJYUkDWAw7Tp4ho3J9xpqIn3zyOc2hI3wQDMC1o9sU5a80Vyc/mEqpE8SQ3
qOWg9Gdx3DXTWOztcx2IxZtFEkIukPM8iD/Fkr//3XHeIc3+mqRAQdY+w7EopzbP
hLwjHVEJs1EMYq8ntWmMFjZ4+ImFgw==
=Peuv
-----END PGP SIGNATURE-----

View file

@ -31,7 +31,7 @@ if [ -z "$VENV_PATH" ]; then
fi
VENV_BIN="$VENV_PATH/bin"
BOOTSTRAP_VERSION_PATH="$VENV_PATH/certbot-auto-bootstrap-version.txt"
LE_AUTO_VERSION="1.10.0.dev0"
LE_AUTO_VERSION="1.11.0.dev0"
BASENAME=$(basename $0)
USAGE="Usage: $BASENAME [OPTIONS]
A self-updating wrapper script for the Certbot ACME client. When run, updates
@ -804,6 +804,7 @@ elif [ -f /etc/mageia-release ]; then
# Mageia has both /etc/mageia-release and /etc/redhat-release
DEPRECATED_OS=1
elif [ -f /etc/redhat-release ]; then
DEPRECATED_OS=1
# Run DeterminePythonVersion to decide on the basis of available Python versions
# whether to use 2.x or 3.x on RedHat-like systems.
# Then, revert LE_PYTHON to its previous state.
@ -836,12 +837,7 @@ elif [ -f /etc/redhat-release ]; then
INTERACTIVE_BOOTSTRAP=1
fi
Bootstrap() {
BootstrapMessage "Legacy RedHat-based OSes that will use Python3"
BootstrapRpmPython3Legacy
}
USE_PYTHON_3=1
BOOTSTRAP_VERSION="BootstrapRpmPython3Legacy $BOOTSTRAP_RPM_PYTHON3_LEGACY_VERSION"
# Try now to enable SCL rh-python36 for systems already bootstrapped
# NB: EnablePython36SCL has been defined along with BootstrapRpmPython3Legacy in certbot-auto
@ -860,18 +856,7 @@ elif [ -f /etc/redhat-release ]; then
fi
if [ "$RPM_USE_PYTHON_3" = 1 ]; then
Bootstrap() {
BootstrapMessage "RedHat-based OSes that will use Python3"
BootstrapRpmPython3
}
USE_PYTHON_3=1
BOOTSTRAP_VERSION="BootstrapRpmPython3 $BOOTSTRAP_RPM_PYTHON3_VERSION"
else
Bootstrap() {
BootstrapMessage "RedHat-based OSes"
BootstrapRpmCommon
}
BOOTSTRAP_VERSION="BootstrapRpmCommon $BOOTSTRAP_RPM_COMMON_VERSION"
fi
fi
@ -889,10 +874,7 @@ elif uname | grep -iq FreeBSD ; then
elif uname | grep -iq Darwin ; then
DEPRECATED_OS=1
elif [ -f /etc/issue ] && grep -iq "Amazon Linux" /etc/issue ; then
Bootstrap() {
ExperimentalBootstrap "Amazon Linux" BootstrapRpmCommon
}
BOOTSTRAP_VERSION="BootstrapRpmCommon $BOOTSTRAP_RPM_COMMON_VERSION"
DEPRECATED_OS=1
elif [ -f /etc/product ] && grep -q "Joyent Instance" /etc/product ; then
DEPRECATED_OS=1
else
@ -1493,18 +1475,18 @@ letsencrypt==0.7.0 \
--hash=sha256:105a5fb107e45bcd0722eb89696986dcf5f08a86a321d6aef25a0c7c63375ade \
--hash=sha256:c36e532c486a7e92155ee09da54b436a3c420813ec1c590b98f635d924720de9
certbot==1.9.0 \
--hash=sha256:d5a804d32e471050921f7b39ed9859e2e9de02824176ed78f57266222036b53a \
--hash=sha256:2ff9bf7d9af381c7efee22dec2dd6938d9d8fddcc9e11682b86e734164a30b57
acme==1.9.0 \
--hash=sha256:d8061b396a22b21782c9b23ff9a945b23e50fca2573909a42f845e11d5658ac5 \
--hash=sha256:38a1630c98e144136c62eec4d2c545a1bdb1a3cd4eca82214be6b83a1f5a161f
certbot-apache==1.9.0 \
--hash=sha256:09528a820d57e54984d490100644cd8a6603db97bf5776f86e95795ecfacf23d \
--hash=sha256:f47fb3f4a9bd927f4812121a0beefe56b163475a28f4db34c64dc838688d9e9e
certbot-nginx==1.9.0 \
--hash=sha256:bb2e3f7fe17f071f350a3efa48571b8ef40a8e4b6db9c6da72539206a20b70be \
--hash=sha256:ab26a4f49d53b0e8bf0f903e58e2a840cda233fe1cbbc54c36ff17f973e57d65
certbot==1.10.1 \
--hash=sha256:011ac980fa21b9f29e02c9b8d8b86e8a4bf4670b51b6ad91656e401e9d2d2231 \
--hash=sha256:0d9ee3fc09e0d03b2d1b1f1c4916e61ecfc6904b4216ddef4e6a5ca1424d9cb7
acme==1.10.1 \
--hash=sha256:752d598e54e98ad1e874de53fd50c61044f1b566d6deb790db5676ce9c573546 \
--hash=sha256:fcbb559aedc96b404edf593e78517dcd7291984d5a37036c3fc77f3c5c122fd8
certbot-apache==1.10.1 \
--hash=sha256:f077b4b7f166627ef5e0921fe7cde57700670fc86e9ad9dbdfaf2c573cc0f2fa \
--hash=sha256:97ed637b4c7b03820db6c69aa90145dc989933351d46a3d62baf6b71674f0a10
certbot-nginx==1.10.1 \
--hash=sha256:7c36459021f8a1ec3b6c062e4c4fc866bfaa1dbf26ccd29e043dd6848003be08 \
--hash=sha256:c0bbeccf85f46b728fd95e6bb8c2649d32d3383d7f47ea4b9c312d12bf04d2f0
UNLIKELY_EOF
# -------------------------------------------------------------------------

View file

@ -326,6 +326,7 @@ elif [ -f /etc/mageia-release ]; then
# Mageia has both /etc/mageia-release and /etc/redhat-release
DEPRECATED_OS=1
elif [ -f /etc/redhat-release ]; then
DEPRECATED_OS=1
# Run DeterminePythonVersion to decide on the basis of available Python versions
# whether to use 2.x or 3.x on RedHat-like systems.
# Then, revert LE_PYTHON to its previous state.
@ -358,12 +359,7 @@ elif [ -f /etc/redhat-release ]; then
INTERACTIVE_BOOTSTRAP=1
fi
Bootstrap() {
BootstrapMessage "Legacy RedHat-based OSes that will use Python3"
BootstrapRpmPython3Legacy
}
USE_PYTHON_3=1
BOOTSTRAP_VERSION="BootstrapRpmPython3Legacy $BOOTSTRAP_RPM_PYTHON3_LEGACY_VERSION"
# Try now to enable SCL rh-python36 for systems already bootstrapped
# NB: EnablePython36SCL has been defined along with BootstrapRpmPython3Legacy in certbot-auto
@ -382,18 +378,7 @@ elif [ -f /etc/redhat-release ]; then
fi
if [ "$RPM_USE_PYTHON_3" = 1 ]; then
Bootstrap() {
BootstrapMessage "RedHat-based OSes that will use Python3"
BootstrapRpmPython3
}
USE_PYTHON_3=1
BOOTSTRAP_VERSION="BootstrapRpmPython3 $BOOTSTRAP_RPM_PYTHON3_VERSION"
else
Bootstrap() {
BootstrapMessage "RedHat-based OSes"
BootstrapRpmCommon
}
BOOTSTRAP_VERSION="BootstrapRpmCommon $BOOTSTRAP_RPM_COMMON_VERSION"
fi
fi
@ -411,10 +396,7 @@ elif uname | grep -iq FreeBSD ; then
elif uname | grep -iq Darwin ; then
DEPRECATED_OS=1
elif [ -f /etc/issue ] && grep -iq "Amazon Linux" /etc/issue ; then
Bootstrap() {
ExperimentalBootstrap "Amazon Linux" BootstrapRpmCommon
}
BOOTSTRAP_VERSION="BootstrapRpmCommon $BOOTSTRAP_RPM_COMMON_VERSION"
DEPRECATED_OS=1
elif [ -f /etc/product ] && grep -q "Joyent Instance" /etc/product ; then
DEPRECATED_OS=1
else

View file

@ -1,12 +1,12 @@
certbot==1.9.0 \
--hash=sha256:d5a804d32e471050921f7b39ed9859e2e9de02824176ed78f57266222036b53a \
--hash=sha256:2ff9bf7d9af381c7efee22dec2dd6938d9d8fddcc9e11682b86e734164a30b57
acme==1.9.0 \
--hash=sha256:d8061b396a22b21782c9b23ff9a945b23e50fca2573909a42f845e11d5658ac5 \
--hash=sha256:38a1630c98e144136c62eec4d2c545a1bdb1a3cd4eca82214be6b83a1f5a161f
certbot-apache==1.9.0 \
--hash=sha256:09528a820d57e54984d490100644cd8a6603db97bf5776f86e95795ecfacf23d \
--hash=sha256:f47fb3f4a9bd927f4812121a0beefe56b163475a28f4db34c64dc838688d9e9e
certbot-nginx==1.9.0 \
--hash=sha256:bb2e3f7fe17f071f350a3efa48571b8ef40a8e4b6db9c6da72539206a20b70be \
--hash=sha256:ab26a4f49d53b0e8bf0f903e58e2a840cda233fe1cbbc54c36ff17f973e57d65
certbot==1.10.1 \
--hash=sha256:011ac980fa21b9f29e02c9b8d8b86e8a4bf4670b51b6ad91656e401e9d2d2231 \
--hash=sha256:0d9ee3fc09e0d03b2d1b1f1c4916e61ecfc6904b4216ddef4e6a5ca1424d9cb7
acme==1.10.1 \
--hash=sha256:752d598e54e98ad1e874de53fd50c61044f1b566d6deb790db5676ce9c573546 \
--hash=sha256:fcbb559aedc96b404edf593e78517dcd7291984d5a37036c3fc77f3c5c122fd8
certbot-apache==1.10.1 \
--hash=sha256:f077b4b7f166627ef5e0921fe7cde57700670fc86e9ad9dbdfaf2c573cc0f2fa \
--hash=sha256:97ed637b4c7b03820db6c69aa90145dc989933351d46a3d62baf6b71674f0a10
certbot-nginx==1.10.1 \
--hash=sha256:7c36459021f8a1ec3b6c062e4c4fc866bfaa1dbf26ccd29e043dd6848003be08 \
--hash=sha256:c0bbeccf85f46b728fd95e6bb8c2649d32d3383d7f47ea4b9c312d12bf04d2f0

View file

@ -1,7 +0,0 @@
"""Tests for letsencrypt-auto
Run these locally by saying... ::
./build.py && docker build -t lea . -f Dockerfile.<distro> && docker run --rm -t -i lea
"""

Some files were not shown because too many files have changed in this diff Show more