Compare commits

..

6 Commits

Author SHA1 Message Date
Adrien Ferrand
9ae12c010c Merge branch 'master' into drop-python37 2023-10-10 16:05:44 +02:00
Adrien Ferrand
8691d3a657 Update oldest constaints and compatibility tests runtime 2023-10-10 12:32:55 +02:00
Adrien Ferrand
82813f3f36 Update requirements 2023-10-10 10:36:05 +02:00
Adrien Ferrand
10f58b212b Check for venv generation 2023-10-10 10:02:26 +02:00
Adrien Ferrand
8347ddb436 Fix lint and test 2023-10-09 16:32:22 +02:00
Adrien Ferrand
35513f4d0a Drop Python 3.7 support 2023-10-09 15:38:57 +02:00
123 changed files with 1086 additions and 1245 deletions

View File

@@ -15,5 +15,5 @@ variables:
stages:
- template: templates/stages/test-and-package-stage.yml
- template: templates/stages/nightly-deploy-stage.yml
- template: templates/stages/deploy-stage.yml
- template: templates/stages/notify-failure-stage.yml

View File

@@ -13,5 +13,7 @@ variables:
stages:
- template: templates/stages/test-and-package-stage.yml
- template: templates/stages/changelog-stage.yml
- template: templates/stages/release-deploy-stage.yml
- template: templates/stages/deploy-stage.yml
parameters:
snapReleaseChannel: beta
- template: templates/stages/notify-failure-stage.yml

View File

@@ -4,7 +4,7 @@ jobs:
- name: IMAGE_NAME
value: ubuntu-22.04
- name: PYTHON_VERSION
value: 3.12
value: 3.11
- group: certbot-common
strategy:
matrix:
@@ -14,31 +14,32 @@ jobs:
linux-py310:
PYTHON_VERSION: 3.10
TOXENV: py310
linux-py311:
PYTHON_VERSION: 3.11
TOXENV: py311
linux-isolated:
TOXENV: 'isolated-acme,isolated-certbot,isolated-apache,isolated-cloudflare,isolated-digitalocean,isolated-dnsimple,isolated-dnsmadeeasy,isolated-gehirn,isolated-google,isolated-linode,isolated-luadns,isolated-nsone,isolated-ovh,isolated-rfc2136,isolated-route53,isolated-sakuracloud,isolated-nginx'
linux-integration-certbot-oldest:
TOXENV: 'isolated-{acme,certbot,apache,cloudflare,digitalocean,dnsimple,dnsmadeeasy,gehirn,google,linode,luadns,nsone,ovh,rfc2136,route53,sakuracloud,nginx}'
linux-boulder-v2-integration-certbot-oldest:
PYTHON_VERSION: 3.8
TOXENV: integration-certbot-oldest
linux-integration-nginx-oldest:
ACME_SERVER: boulder-v2
linux-boulder-v2-integration-nginx-oldest:
PYTHON_VERSION: 3.8
TOXENV: integration-nginx-oldest
# python 3.8 integration tests are not run here because they're run as
# part of the standard test suite
linux-py39-integration:
ACME_SERVER: boulder-v2
linux-boulder-v2-py38-integration:
PYTHON_VERSION: 3.8
TOXENV: integration
ACME_SERVER: boulder-v2
linux-boulder-v2-py39-integration:
PYTHON_VERSION: 3.9
TOXENV: integration
linux-py310-integration:
ACME_SERVER: boulder-v2
linux-boulder-v2-py310-integration:
PYTHON_VERSION: 3.10
TOXENV: integration
linux-py311-integration:
ACME_SERVER: boulder-v2
linux-boulder-v2-py311-integration:
PYTHON_VERSION: 3.11
TOXENV: integration
linux-py312-integration:
PYTHON_VERSION: 3.12
TOXENV: integration
ACME_SERVER: boulder-v2
nginx-compat:
TOXENV: nginx_compat
linux-integration-rfc2136:

View File

@@ -55,6 +55,65 @@ jobs:
- bash: |
set -e && tools/docker/test.sh $(dockerTag) $DOCKER_ARCH
displayName: Run integration tests for Docker images
- job: installer_build
pool:
vmImage: windows-2019
steps:
- task: UsePythonVersion@0
inputs:
versionSpec: 3.9
architecture: x64
addToPath: true
- script: |
python -m venv venv
venv\Scripts\python tools\pip_install.py -e windows-installer
displayName: Prepare Windows installer build environment
- script: |
venv\Scripts\construct-windows-installer
displayName: Build Certbot installer
- task: CopyFiles@2
inputs:
sourceFolder: $(System.DefaultWorkingDirectory)/windows-installer/build/nsis
contents: '*.exe'
targetFolder: $(Build.ArtifactStagingDirectory)
- task: PublishPipelineArtifact@1
inputs:
path: $(Build.ArtifactStagingDirectory)
# If we change the artifact's name, it should also be changed in tools/create_github_release.py
artifact: windows-installer
displayName: Publish Windows installer
- job: installer_run
dependsOn: installer_build
strategy:
matrix:
win2019:
imageName: windows-2019
pool:
vmImage: $(imageName)
steps:
- task: UsePythonVersion@0
inputs:
versionSpec: 3.9
addToPath: true
- task: DownloadPipelineArtifact@2
inputs:
artifact: windows-installer
path: $(Build.SourcesDirectory)/bin
displayName: Retrieve Windows installer
- script: |
python -m venv venv
venv\Scripts\python tools\pip_install.py -e certbot-ci
env:
PIP_NO_BUILD_ISOLATION: no
displayName: Prepare Certbot-CI
- script: |
set PATH=%ProgramFiles%\Certbot\bin;%PATH%
venv\Scripts\python -m pytest certbot-ci\windows_installer_integration_tests --allow-persistent-changes --installer-path $(Build.SourcesDirectory)\bin\certbot-beta-installer-win_amd64.exe
displayName: Run windows installer integration tests
- script: |
set PATH=%ProgramFiles%\Certbot\bin;%PATH%
venv\Scripts\python -m pytest certbot-ci\certbot_integration_tests\certbot_tests -n 4
displayName: Run certbot integration tests
- job: snaps_build
pool:
vmImage: ubuntu-22.04
@@ -76,7 +135,7 @@ jobs:
displayName: Install dependencies
- task: UsePythonVersion@0
inputs:
versionSpec: 3.12
versionSpec: 3.8
addToPath: true
- task: DownloadSecureFile@1
name: credentials
@@ -107,7 +166,7 @@ jobs:
steps:
- task: UsePythonVersion@0
inputs:
versionSpec: 3.12
versionSpec: 3.8
addToPath: true
- script: |
set -e
@@ -127,7 +186,7 @@ jobs:
displayName: Install Certbot snap
- script: |
set -e
venv/bin/python -m tox run -e integration-external,apacheconftest-external-with-pebble
venv/bin/python -m tox -e integration-external,apacheconftest-external-with-pebble
displayName: Run tox
- job: snap_dns_run
dependsOn: snaps_build
@@ -141,7 +200,7 @@ jobs:
displayName: Install dependencies
- task: UsePythonVersion@0
inputs:
versionSpec: 3.12
versionSpec: 3.8
addToPath: true
- task: DownloadPipelineArtifact@2
inputs:

View File

@@ -72,57 +72,3 @@ jobs:
tools/retry.sh eval snapcraft upload --release=${{ parameters.snapReleaseChannel }} "${SNAP_FILE}"
done
displayName: Publish to Snap store
# The credentials used in the following jobs are for the shared
# certbotbot account on Docker Hub. The credentials are stored
# in a service account which was created by following the
# instructions at
# https://docs.microsoft.com/en-us/azure/devops/pipelines/library/service-endpoints?view=azure-devops&tabs=yaml#sep-docreg.
# The name given to this service account must match the value
# given to containerRegistry below. The authentication used when
# creating this service account was a personal access token
# rather than a password to bypass 2FA. When Brad set this up,
# Azure Pipelines failed to verify the credentials with an error
# like "access is forbidden with a JWT issued from a personal
# access token", but after saving them without verification, the
# access token worked when the pipeline actually ran. "Grant
# access to all pipelines" should also be checked on the service
# account. The access token can be deleted on Docker Hub if
# these credentials need to be revoked.
- job: publish_docker_by_arch
pool:
vmImage: ubuntu-22.04
strategy:
matrix:
arm32v6:
DOCKER_ARCH: arm32v6
arm64v8:
DOCKER_ARCH: arm64v8
amd64:
DOCKER_ARCH: amd64
steps:
- task: DownloadPipelineArtifact@2
inputs:
artifact: docker_$(DOCKER_ARCH)
path: $(Build.SourcesDirectory)
displayName: Retrieve Docker images
- bash: set -e && docker load --input $(Build.SourcesDirectory)/images.tar
displayName: Load Docker images
- task: Docker@2
inputs:
command: login
containerRegistry: docker-hub
displayName: Login to Docker Hub
- bash: set -e && tools/docker/deploy_images.sh $(dockerTag) $DOCKER_ARCH
displayName: Deploy the Docker images by architecture
- job: publish_docker_multiarch
dependsOn: publish_docker_by_arch
pool:
vmImage: ubuntu-22.04
steps:
- task: Docker@2
inputs:
command: login
containerRegistry: docker-hub
displayName: Login to Docker Hub
- bash: set -e && tools/docker/deploy_manifests.sh $(dockerTag) all
displayName: Deploy the Docker multiarch manifests

View File

@@ -1,7 +1,7 @@
jobs:
- job: test
variables:
PYTHON_VERSION: 3.12
PYTHON_VERSION: 3.11
strategy:
matrix:
macos-py38-cover:
@@ -12,10 +12,22 @@ jobs:
# See https://github.com/certbot/certbot/pull/9717#issuecomment-1610861794.
PIP_USE_PEP517: "true"
macos-cover:
IMAGE_NAME: macOS-13
IMAGE_NAME: macOS-12
TOXENV: cover
# See explanation under macos-py38-cover.
PIP_USE_PEP517: "true"
windows-py38:
IMAGE_NAME: windows-2019
PYTHON_VERSION: 3.8
TOXENV: py-win
windows-py39-cover:
IMAGE_NAME: windows-2019
PYTHON_VERSION: 3.9
TOXENV: cover-win
windows-integration-certbot:
IMAGE_NAME: windows-2019
PYTHON_VERSION: 3.9
TOXENV: integration-certbot
linux-oldest:
IMAGE_NAME: ubuntu-22.04
PYTHON_VERSION: 3.8
@@ -37,6 +49,7 @@ jobs:
IMAGE_NAME: ubuntu-22.04
PYTHON_VERSION: 3.8
TOXENV: integration
ACME_SERVER: pebble
apache-compat:
IMAGE_NAME: ubuntu-22.04
TOXENV: apache_compat

View File

@@ -0,0 +1,67 @@
parameters:
# We do not define acceptable values for this parameter here as it is passed
# through to ../jobs/snap-deploy-job.yml which does its own sanity checking.
- name: snapReleaseChannel
type: string
default: edge
stages:
- stage: Deploy
jobs:
- template: ../jobs/snap-deploy-job.yml
parameters:
snapReleaseChannel: ${{ parameters.snapReleaseChannel }}
# The credentials used in the following jobs are for the shared
# certbotbot account on Docker Hub. The credentials are stored
# in a service account which was created by following the
# instructions at
# https://docs.microsoft.com/en-us/azure/devops/pipelines/library/service-endpoints?view=azure-devops&tabs=yaml#sep-docreg.
# The name given to this service account must match the value
# given to containerRegistry below. The authentication used when
# creating this service account was a personal access token
# rather than a password to bypass 2FA. When Brad set this up,
# Azure Pipelines failed to verify the credentials with an error
# like "access is forbidden with a JWT issued from a personal
# access token", but after saving them without verification, the
# access token worked when the pipeline actually ran. "Grant
# access to all pipelines" should also be checked on the service
# account. The access token can be deleted on Docker Hub if
# these credentials need to be revoked.
- job: publish_docker_by_arch
pool:
vmImage: ubuntu-22.04
strategy:
matrix:
arm32v6:
DOCKER_ARCH: arm32v6
arm64v8:
DOCKER_ARCH: arm64v8
amd64:
DOCKER_ARCH: amd64
steps:
- task: DownloadPipelineArtifact@2
inputs:
artifact: docker_$(DOCKER_ARCH)
path: $(Build.SourcesDirectory)
displayName: Retrieve Docker images
- bash: set -e && docker load --input $(Build.SourcesDirectory)/images.tar
displayName: Load Docker images
- task: Docker@2
inputs:
command: login
containerRegistry: docker-hub
displayName: Login to Docker Hub
- bash: set -e && tools/docker/deploy_images.sh $(dockerTag) $DOCKER_ARCH
displayName: Deploy the Docker images by architecture
- job: publish_docker_multiarch
dependsOn: publish_docker_by_arch
pool:
vmImage: ubuntu-22.04
steps:
- task: Docker@2
inputs:
command: login
containerRegistry: docker-hub
displayName: Login to Docker Hub
- bash: set -e && tools/docker/deploy_manifests.sh $(dockerTag) all
displayName: Deploy the Docker multiarch manifests

View File

@@ -1,6 +0,0 @@
stages:
- stage: Deploy
jobs:
- template: ../jobs/common-deploy-jobs.yml
parameters:
snapReleaseChannel: edge

View File

@@ -1,38 +0,0 @@
stages:
- stage: Deploy
jobs:
- template: ../jobs/common-deploy-jobs.yml
parameters:
snapReleaseChannel: beta
- job: create_github_release
pool:
vmImage: ubuntu-22.04
steps:
- task: DownloadPipelineArtifact@2
inputs:
artifact: changelog
path: '$(Pipeline.Workspace)'
- task: GitHubRelease@1
inputs:
# this "github-releases" credential is what azure pipelines calls a
# "service connection". it was created using the instructions at
# https://learn.microsoft.com/en-us/azure/devops/pipelines/library/service-endpoints?view=azure-devops&tabs=yaml#github-service-connection
# with a fine-grained personal access token from github to limit
# the permissions given to azure pipelines. the connection on azure
# needs permissions for the "release" pipeline (and maybe the
# "full-test-suite" pipeline to simplify testing it). information
# on how to set up these permissions can be found at
# https://learn.microsoft.com/en-us/azure/devops/pipelines/library/service-endpoints?view=azure-devops&tabs=yaml#secure-a-service-connection.
# the github token that is used needs "contents:write" and
# "workflows:write" permissions for the certbot repo
#
# as of writing this, the current token will expire on 3/15/2025.
# when recreating it, you may also want to create it using the
# shared "certbotbot" github account so the credentials aren't tied
# to any one dev's github account and their access to the certbot
# repo
gitHubConnection: github-releases
title: ${{ format('Certbot {0}', replace(variables['Build.SourceBranchName'], 'v', '')) }}
releaseNotesFilePath: '$(Pipeline.Workspace)/release_notes.md'
assets: '$(Build.SourcesDirectory)/packages/{*.tar.gz,SHA256SUMS*}'
addChangeLog: false

View File

@@ -44,7 +44,7 @@ steps:
export TARGET_BRANCH="`echo "${BUILD_SOURCEBRANCH}" | sed -E 's!refs/(heads|tags)/!!g'`"
[ -z "${SYSTEM_PULLREQUEST_TARGETBRANCH}" ] || export TARGET_BRANCH="${SYSTEM_PULLREQUEST_TARGETBRANCH}"
env
python3 -m tox run
python3 -m tox
env:
AWS_ACCESS_KEY_ID: $(AWS_ACCESS_KEY_ID)
AWS_SECRET_ACCESS_KEY: $(AWS_SECRET_ACCESS_KEY)

View File

@@ -11,11 +11,22 @@ jobs:
if: github.event.pull_request.merged == true && !github.event.pull_request.head.repo.fork
runs-on: ubuntu-latest
steps:
- name: Create Mattermost Message
# https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions#example-of-a-script-injection-attack
env:
NUMBER: ${{ github.event.number }}
PR_URL: https://github.com/${{ github.repository }}/pull/${{ github.event.number }}
REPO: ${{ github.repository }}
USER: ${{ github.actor }}
TITLE: ${{ github.event.pull_request.title }}
run: |
jq --null-input \
--arg number "$NUMBER" \
--arg pr_url "$PR_URL" \
--arg repo "$REPO" \
--arg user "$USER" \
--arg title "$TITLE" \
'{ "text": "[\($repo)] | [\($title) #\($number)](\($pr_url)) was merged into master by \($user)" }' > mattermost.json
- uses: mattermost/action-mattermost-notify@master
with:
env:
MATTERMOST_WEBHOOK_URL: ${{ secrets.MATTERMOST_MERGE_WEBHOOK }}
TEXT: >
[${{ github.repository }}] |
[${{ github.event.pull_request.title }}
#${{ github.event.number }}](https://github.com/${{ github.repository }}/pull/${{ github.event.number }}
was merged into master by ${{ github.actor }}

View File

@@ -69,7 +69,7 @@ ignored-modules=
# CERTBOT COMMENT
# This is needed for pylint to import linter_plugin.py since
# https://github.com/PyCQA/pylint/pull/3396.
init-hook="import pylint.config, os, sys; sys.path.append(os.path.dirname(next(pylint.config.find_default_config_files())))"
init-hook="import pylint.config, os, sys; sys.path.append(os.path.dirname(pylint.config.PYLINTRC))"
# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the
# number of processors available to use, and will cap the count on Windows to
@@ -266,8 +266,8 @@ valid-metaclass-classmethod-first-arg=cls
[EXCEPTIONS]
# Exceptions that will emit a warning when caught.
overgeneral-exceptions=builtins.BaseException,
builtins.Exception
overgeneral-exceptions=BaseException,
Exception
[FORMAT]
@@ -524,7 +524,7 @@ ignored-classes=optparse.Values,thread._local,_thread._local,argparse.Namespace,
# List of module names for which member attributes should not be checked
# (useful for modules/projects where namespaces are manipulated during runtime
# and thus existing member attributes cannot be deduced by static analysis
ignored-modules=confargparse,argparse
ignored-modules=pkg_resources,confargparse,argparse
# Show a hint with possible names when a member name was not found. The aspect
# of finding the hint is based on edit distance.

View File

@@ -94,7 +94,6 @@ Authors
* [Felix Yan](https://github.com/felixonmars)
* [Filip Ochnik](https://github.com/filipochnik)
* [Florian Klink](https://github.com/flokli)
* [Francesco Colista](https://github.com/fcolista)
* [Francois Marier](https://github.com/fmarier)
* [Frank](https://github.com/Frankkkkk)
* [Frederic BLANC](https://github.com/fblanc)
@@ -166,7 +165,6 @@ Authors
* [Luca Ebach](https://github.com/lucebac)
* [Luca Olivetti](https://github.com/olivluca)
* [Luke Rogers](https://github.com/lukeroge)
* [Lukhnos Liu](https://github.com/lukhnos)
* [Maarten](https://github.com/mrtndwrd)
* [Mads Jensen](https://github.com/atombrella)
* [Maikel Martens](https://github.com/krukas)

View File

@@ -14,14 +14,14 @@ build:
# Build documentation in the "docs/" directory with Sphinx
sphinx:
configuration: acme/docs/conf.py
configuration: docs/conf.py
# You can configure Sphinx to use a different builder, for instance use the dirhtml builder for simpler URLs
# builder: "dirhtml"
# Fail on all warnings to avoid broken references
fail_on_warning: true
# Optionally build your docs in additional formats such as PDF and ePub
formats:
# formats:
- pdf
- epub
@@ -30,4 +30,4 @@ formats:
# See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html
python:
install:
- requirements: acme/readthedocs.org.requirements.txt
- requirements: ../tools/requirements.txt

View File

@@ -12,6 +12,7 @@ from typing import List
from typing import Mapping
from typing import Optional
from typing import Set
from typing import Text
from typing import Tuple
from typing import Union
@@ -516,7 +517,7 @@ class ClientNetwork:
self.account = account
self.alg = alg
self.verify_ssl = verify_ssl
self._nonces: Set[str] = set()
self._nonces: Set[Text] = set()
self.user_agent = user_agent
self.session = requests.Session()
self._default_timeout = timeout

View File

@@ -29,7 +29,7 @@ class Header(jose.Header):
class Signature(jose.Signature):
"""ACME-specific Signature. Uses ACME-specific Header for customer fields."""
__slots__ = jose.Signature._orig_slots # pylint: disable=protected-access,no-member
__slots__ = jose.Signature._orig_slots # type: ignore[attr-defined] # pylint: disable=protected-access,no-member
# TODO: decoder/encoder should accept cls? Otherwise, subclassing
# JSONObjectWithFields is tricky...
@@ -44,7 +44,7 @@ class Signature(jose.Signature):
class JWS(jose.JWS):
"""ACME-specific JWS. Includes none, url, and kid in protected header."""
signature_cls = Signature
__slots__ = jose.JWS._orig_slots # pylint: disable=protected-access
__slots__ = jose.JWS._orig_slots # type: ignore[attr-defined] # pylint: disable=protected-access
@classmethod
# type: ignore[override] # pylint: disable=arguments-differ

View File

@@ -3,13 +3,11 @@ import sys
from setuptools import find_packages
from setuptools import setup
version = '2.12.0.dev0'
version = '2.8.0.dev0'
install_requires = [
'cryptography>=3.2.1',
# Josepy 2+ may introduce backward incompatible changes by droping usage of
# deprecated PyOpenSSL APIs.
'josepy>=1.13.0, <2',
'josepy>=1.13.0',
# pyOpenSSL 23.1.0 is a bad release: https://github.com/pyca/pyopenssl/issues/1199
'PyOpenSSL>=17.5.0,!=23.1.0',
'pyrfc3339',
@@ -57,7 +55,6 @@ setup(
'Programming Language :: Python :: 3.9',
'Programming Language :: Python :: 3.10',
'Programming Language :: Python :: 3.11',
'Programming Language :: Python :: 3.12',
'Topic :: Internet :: WWW/HTTP',
'Topic :: Security',
],

View File

@@ -257,6 +257,6 @@ def find_ssl_apache_conf(prefix: str) -> str:
"""
file_manager = ExitStack()
atexit.register(file_manager.close)
ref = (importlib_resources.files("certbot_apache").joinpath("_internal")
.joinpath("tls_configs").joinpath("{0}-options-ssl-apache.conf".format(prefix)))
ref = importlib_resources.files("certbot_apache").joinpath(
"_internal", "tls_configs", "{0}-options-ssl-apache.conf".format(prefix))
return str(file_manager.enter_context(importlib_resources.as_file(ref)))

View File

@@ -4,7 +4,6 @@ from typing import Type
from certbot import util
from certbot_apache._internal import configurator
from certbot_apache._internal import override_alpine
from certbot_apache._internal import override_arch
from certbot_apache._internal import override_centos
from certbot_apache._internal import override_darwin
@@ -15,7 +14,6 @@ from certbot_apache._internal import override_suse
from certbot_apache._internal import override_void
OVERRIDE_CLASSES: Dict[str, Type[configurator.ApacheConfigurator]] = {
"alpine": override_alpine.AlpineConfigurator,
"arch": override_arch.ArchConfigurator,
"cloudlinux": override_centos.CentOSConfigurator,
"darwin": override_darwin.DarwinConfigurator,

View File

@@ -1,19 +0,0 @@
""" Distribution specific override class for Alpine Linux """
from certbot_apache._internal import configurator
from certbot_apache._internal.configurator import OsOptions
class AlpineConfigurator(configurator.ApacheConfigurator):
"""Alpine Linux specific ApacheConfigurator override class"""
OS_DEFAULTS = OsOptions(
server_root="/etc/apache2",
vhost_root="/etc/apache2/conf.d",
vhost_files="*.conf",
logs_root="/var/log/apache2",
ctl="apachectl",
version_cmd=['apachectl', '-v'],
restart_cmd=['apachectl', 'graceful'],
conftest_cmd=['apachectl', 'configtest'],
challenge_location="/etc/apache2/conf.d",
)

View File

@@ -14,7 +14,7 @@ SCRIPT_DIRNAME = os.path.dirname(__file__)
def main() -> int:
args = sys.argv[1:]
with acme_server.ACMEServer([], False) as acme_xdist:
with acme_server.ACMEServer('pebble', [], False) as acme_xdist:
environ = os.environ.copy()
environ['SERVER'] = acme_xdist['directory_url']
command = [os.path.join(SCRIPT_DIRNAME, 'apache-conf-test')]

View File

@@ -1,7 +1,7 @@
from setuptools import find_packages
from setuptools import setup
version = '2.12.0.dev0'
version = '2.8.0.dev0'
install_requires = [
# We specify the minimum acme and certbot version as the current plugin
@@ -43,7 +43,6 @@ setup(
'Programming Language :: Python :: 3.9',
'Programming Language :: Python :: 3.10',
'Programming Language :: Python :: 3.11',
'Programming Language :: Python :: 3.12',
'Topic :: Internet :: WWW/HTTP',
'Topic :: Security',
'Topic :: System :: Installation/Setup',

View File

@@ -23,6 +23,7 @@ class IntegrationTestsContext:
self.worker_id = 'primary'
acme_xdist = request.config.acme_xdist # type: ignore[attr-defined]
self.acme_server = acme_xdist['acme_server']
self.directory_url = acme_xdist['directory_url']
self.tls_alpn_01_port = acme_xdist['https_port'][self.worker_id]
self.http_01_port = acme_xdist['http_port'][self.worker_id]

View File

@@ -7,6 +7,7 @@ import shutil
import subprocess
import time
from typing import Generator
from typing import Iterable
from typing import Tuple
from typing import Type
@@ -81,9 +82,11 @@ def test_registration_override(context: IntegrationTestsContext) -> None:
context.certbot(['update_account', '--email', 'ex1@domain.org,ex2@domain.org'])
stdout2, _ = context.certbot(['show_account'])
assert 'example@domain.org' in stdout1, "New email should be present"
assert 'example@domain.org' not in stdout2, "Old email should not be present"
assert 'ex1@domain.org, ex2@domain.org' in stdout2, "New emails should be present"
# https://github.com/letsencrypt/boulder/issues/6144
if context.acme_server != 'boulder-v2':
assert 'example@domain.org' in stdout1, "New email should be present"
assert 'example@domain.org' not in stdout2, "Old email should not be present"
assert 'ex1@domain.org, ex2@domain.org' in stdout2, "New emails should be present"
def test_prepare_plugins(context: IntegrationTestsContext) -> None:
@@ -563,15 +566,19 @@ def test_default_rsa_size(context: IntegrationTestsContext) -> None:
assert_rsa_key(key1, 2048)
@pytest.mark.parametrize('curve,curve_cls', [
@pytest.mark.parametrize('curve,curve_cls,skip_servers', [
# Curve name, Curve class, ACME servers to skip
('secp256r1', SECP256R1),
('secp384r1', SECP384R1),
('secp521r1', SECP521R1)]
('secp256r1', SECP256R1, []),
('secp384r1', SECP384R1, []),
('secp521r1', SECP521R1, ['boulder-v2'])]
)
def test_ecdsa_curves(context: IntegrationTestsContext, curve: str,
curve_cls: Type[EllipticCurve]) -> None:
def test_ecdsa_curves(context: IntegrationTestsContext, curve: str, curve_cls: Type[EllipticCurve],
skip_servers: Iterable[str]) -> None:
"""Test issuance for each supported ECDSA curve"""
if context.acme_server in skip_servers:
pytest.skip('ACME server {} does not support ECDSA curve {}'
.format(context.acme_server, curve))
domain = context.get_domain('curve')
context.certbot([
'certonly',
@@ -633,6 +640,9 @@ def test_renew_with_ec_keys(context: IntegrationTestsContext) -> None:
def test_ocsp_must_staple(context: IntegrationTestsContext) -> None:
"""Test that OCSP Must-Staple is correctly set in the generated certificate."""
if context.acme_server == 'pebble':
pytest.skip('Pebble does not support OCSP Must-Staple.')
certname = context.get_domain('must-staple')
context.certbot(['auth', '--must-staple', '--domains', certname])
@@ -700,14 +710,17 @@ def test_revoke_and_unregister(context: IntegrationTestsContext) -> None:
assert cert3 in stdout
@pytest.mark.parametrize('curve,curve_cls', [
('secp256r1', SECP256R1),
('secp384r1', SECP384R1),
('secp521r1', SECP521R1)]
@pytest.mark.parametrize('curve,curve_cls,skip_servers', [
('secp256r1', SECP256R1, []),
('secp384r1', SECP384R1, []),
('secp521r1', SECP521R1, ['boulder-v2'])]
)
def test_revoke_ecdsa_cert_key(
context: IntegrationTestsContext, curve: str, curve_cls: Type[EllipticCurve]) -> None:
context: IntegrationTestsContext, curve: str, curve_cls: Type[EllipticCurve],
skip_servers: Iterable[str]) -> None:
"""Test revoking a certificate """
if context.acme_server in skip_servers:
pytest.skip(f'ACME server {context.acme_server} does not support ECDSA curve {curve}')
cert: str = context.get_domain('curve')
context.certbot([
'certonly',
@@ -725,14 +738,17 @@ def test_revoke_ecdsa_cert_key(
assert stdout.count('INVALID: REVOKED') == 1, 'Expected {0} to be REVOKED'.format(cert)
@pytest.mark.parametrize('curve,curve_cls', [
('secp256r1', SECP256R1),
('secp384r1', SECP384R1),
('secp521r1', SECP521R1)]
@pytest.mark.parametrize('curve,curve_cls,skip_servers', [
('secp256r1', SECP256R1, []),
('secp384r1', SECP384R1, []),
('secp521r1', SECP521R1, ['boulder-v2'])]
)
def test_revoke_ecdsa_cert_key_delete(
context: IntegrationTestsContext, curve: str, curve_cls: Type[EllipticCurve]) -> None:
context: IntegrationTestsContext, curve: str, curve_cls: Type[EllipticCurve],
skip_servers: Iterable[str]) -> None:
"""Test revoke and deletion for each supported curve type"""
if context.acme_server in skip_servers:
pytest.skip(f'ACME server {context.acme_server} does not support ECDSA curve {curve}')
cert: str = context.get_domain('curve')
context.certbot([
'certonly',
@@ -897,7 +913,7 @@ def test_dry_run_deactivate_authzs(context: IntegrationTestsContext) -> None:
def test_preferred_chain(context: IntegrationTestsContext) -> None:
"""Test that --preferred-chain results in the correct chain.pem being produced"""
try:
issuers = misc.get_acme_issuers()
issuers = misc.get_acme_issuers(context)
except NotImplementedError:
pytest.skip('This ACME server does not support alternative issuers.')

View File

@@ -8,6 +8,7 @@ for a directory a specific configuration using built-in pytest hooks.
See https://docs.pytest.org/en/latest/reference.html#hook-reference
"""
import contextlib
import subprocess
import sys
from certbot_integration_tests.utils import acme_server as acme_lib
@@ -19,6 +20,10 @@ def pytest_addoption(parser):
Standard pytest hook to add options to the pytest parser.
:param parser: current pytest parser that will be used on the CLI
"""
parser.addoption('--acme-server', default='pebble',
choices=['boulder-v2', 'pebble'],
help='select the ACME server to use (boulder-v2, pebble), '
'defaulting to pebble')
parser.addoption('--dns-server', default='challtestsrv',
choices=['bind', 'challtestsrv'],
help='select the DNS server to use (bind, challtestsrv), '
@@ -64,7 +69,7 @@ def _setup_primary_node(config):
Setup the environment for integration tests.
This function will:
- check runtime compatibility (Docker, docker compose, Nginx)
- check runtime compatibility (Docker, docker-compose, Nginx)
- create a temporary workspace and the persistent GIT repositories space
- configure and start a DNS server using Docker, if configured
- configure and start paralleled ACME CA servers using Docker
@@ -75,6 +80,22 @@ def _setup_primary_node(config):
:param config: Configuration of the pytest primary node. Is modified by this function.
"""
# Check for runtime compatibility: some tools are required to be available in PATH
if 'boulder' in config.option.acme_server:
try:
subprocess.check_output(['docker', '-v'], stderr=subprocess.STDOUT)
except (subprocess.CalledProcessError, OSError):
raise ValueError('Error: docker is required in PATH to launch the integration tests on'
'boulder, but is not installed or not available for current user.')
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.'
)
# Parameter numprocesses is added to option by pytest-xdist
workers = ['primary'] if not config.option.numprocesses\
else ['gw{0}'.format(i) for i in range(config.option.numprocesses)]
@@ -94,7 +115,8 @@ def _setup_primary_node(config):
# By calling setup_acme_server we ensure that all necessary acme server instances will be
# fully started. This runtime is reflected by the acme_xdist returned.
acme_server = acme_lib.ACMEServer(workers, dns_server=acme_dns_server)
acme_server = acme_lib.ACMEServer(config.option.acme_server, workers,
dns_server=acme_dns_server)
config.add_cleanup(acme_server.stop)
print('ACME xdist config:\n{0}'.format(acme_server.acme_xdist))
acme_server.start()

View File

@@ -32,15 +32,13 @@ def construct_nginx_config(nginx_root: str, nginx_webroot: str, http_port: int,
if not key_path:
file_manager = ExitStack()
atexit.register(file_manager.close)
ref = (importlib_resources.files('certbot_integration_tests').joinpath('assets')
.joinpath('key.pem'))
ref = importlib_resources.files('certbot_integration_tests').joinpath('assets', 'key.pem')
key_path = str(file_manager.enter_context(importlib_resources.as_file(ref)))
if not cert_path:
file_manager = ExitStack()
atexit.register(file_manager.close)
ref = (importlib_resources.files('certbot_integration_tests').joinpath('assets')
.joinpath('cert.pem'))
ref = importlib_resources.files('certbot_integration_tests').joinpath('assets', 'cert.pem')
cert_path = str(file_manager.enter_context(importlib_resources.as_file(ref)))
return '''\

View File

@@ -48,8 +48,9 @@ class IntegrationTestsContext(certbot_context.IntegrationTestsContext):
:yields: Path to credentials file
:rtype: str
"""
src_ref_file = (importlib_resources.files('certbot_integration_tests').joinpath('assets')
.joinpath('bind-config').joinpath(f'rfc2136-credentials-{label}.ini.tpl'))
src_ref_file = importlib_resources.files('certbot_integration_tests').joinpath(
'assets', 'bind-config', f'rfc2136-credentials-{label}.ini.tpl'
)
with importlib_resources.as_file(src_ref_file) as src_file:
with open(src_file, 'r') as f:
contents = f.read().format(

View File

@@ -5,6 +5,7 @@ import argparse
import errno
import json
import os
from os.path import join
import shutil
import subprocess
import sys
@@ -17,12 +18,14 @@ from typing import Dict
from typing import List
from typing import Mapping
from typing import Optional
from typing import Tuple
from typing import Type
import requests
# pylint: disable=wildcard-import,unused-wildcard-import
from certbot_integration_tests.utils import misc
from certbot_integration_tests.utils import pebble_artifacts
from certbot_integration_tests.utils import pebble_ocsp_server
from certbot_integration_tests.utils import proxy
from certbot_integration_tests.utils.constants import *
@@ -39,30 +42,34 @@ class ACMEServer:
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, nodes: List[str], http_proxy: bool = True,
def __init__(self, acme_server: str, nodes: List[str], http_proxy: bool = True,
stdout: bool = False, dns_server: Optional[str] = None,
http_01_port: Optional[int] = None) -> None:
"""
Create an ACMEServer instance.
:param str acme_server: the type of acme server used (boulder-v2 or pebble)
:param list nodes: list of node names that will be setup by pytest xdist
: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 will use it to resolve domains
: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(nodes)
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: List[subprocess.Popen] = []
self._stdout = sys.stdout if stdout else open(os.devnull, 'w') # pylint: disable=consider-using-with
self._dns_server = dns_server
self._http_01_port = DEFAULT_HTTP_01_PORT
self._preterminate_cmds_args: List[Tuple[Tuple[Any, ...], Dict[str, Any]]] = []
self._http_01_port = BOULDER_HTTP_01_PORT if self._acme_type == 'boulder' \
else DEFAULT_HTTP_01_PORT
if http_01_port:
if self._proxy:
if (self._acme_type == 'pebble' and self._proxy) or self._acme_type == 'boulder':
raise ValueError('Setting http_01_port is not currently supported when '
'using the HTTP proxy')
'using Boulder or the HTTP proxy')
self._http_01_port = http_01_port
def start(self) -> None:
@@ -70,7 +77,10 @@ class ACMEServer:
try:
if self._proxy:
self._prepare_http_proxy()
self._prepare_pebble_server()
if self._acme_type == 'pebble':
self._prepare_pebble_server()
if self._acme_type == 'boulder':
self._prepare_boulder_server()
except BaseException as e:
self.stop()
raise e
@@ -79,6 +89,7 @@ class ACMEServer:
"""Stop the test stack, and clean its resources"""
print('=> Tear down the test infrastructure...')
try:
self._run_preterminate_cmds()
for process in self._processes:
try:
process.terminate()
@@ -104,14 +115,19 @@ class ACMEServer:
traceback: Optional[TracebackType]) -> None:
self.stop()
def _construct_acme_xdist(self, nodes: List[str]) -> None:
def _construct_acme_xdist(self, acme_server: str, nodes: List[str]) -> None:
"""Generate and return the acme_xdist dict"""
acme_xdist: Dict[str, Any] = {}
acme_xdist: Dict[str, Any] = {'acme_server': acme_server}
# Directory and ACME port are set implicitly in the docker-compose.yml
# files of Pebble.
acme_xdist['directory_url'] = PEBBLE_DIRECTORY_URL
acme_xdist['challtestsrv_url'] = PEBBLE_CHALLTESTSRV_URL
# files of Boulder/Pebble.
if acme_server == 'pebble':
acme_xdist['directory_url'] = PEBBLE_DIRECTORY_URL
acme_xdist['challtestsrv_url'] = PEBBLE_CHALLTESTSRV_URL
else: # boulder
acme_xdist['directory_url'] = BOULDER_V2_DIRECTORY_URL
acme_xdist['challtestsrv_url'] = BOULDER_V2_CHALLTESTSRV_URL
acme_xdist['http_port'] = dict(zip(nodes, range(5200, 5200 + len(nodes))))
acme_xdist['https_port'] = dict(zip(nodes, range(5100, 5100 + len(nodes))))
acme_xdist['other_port'] = dict(zip(nodes, range(5300, 5300 + len(nodes))))
@@ -145,6 +161,11 @@ class ACMEServer:
[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.
from certbot_integration_tests.utils import pebble_ocsp_server
self._launch_process([sys.executable, pebble_ocsp_server.__file__])
# Wait for the ACME CA server to be up.
@@ -153,6 +174,68 @@ class ACMEServer:
print('=> Finished pebble instance deployment.')
def _prepare_boulder_server(self) -> None:
"""Configure and launch the Boulder server"""
print('=> Starting boulder instance deployment...')
instance_path = join(self._workspace, 'boulder')
# Load Boulder from git, that includes a docker-compose.yml ready for production.
process = self._launch_process(['git', 'clone', 'https://github.com/letsencrypt/boulder',
'--single-branch', '--depth=1', instance_path])
process.wait(MAX_SUBPROCESS_WAIT)
# Allow Boulder to ignore usual limit rate policies, useful for tests.
os.rename(join(instance_path, 'test/rate-limit-policies-b.yml'),
join(instance_path, 'test/rate-limit-policies.yml'))
if self._dns_server:
# Change Boulder config to use the provided DNS server
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=(',', ': ')))
# This command needs to be run before we try and terminate running processes because
# docker-compose up doesn't always respond to SIGTERM. See
# https://github.com/certbot/certbot/pull/9435.
self._register_preterminate_cmd(['docker-compose', 'down'], cwd=instance_path)
# Boulder docker generates build artifacts owned by root with 0o744 permissions.
# If we started the acme server from a normal user that has access to the Docker
# daemon, this user will not be able to delete these artifacts from the host.
# We need to do it through a docker.
self._register_preterminate_cmd(['docker', 'run', '--rm', '-v',
'{0}:/workspace'.format(self._workspace), 'alpine', 'rm',
'-rf', '/workspace/boulder'])
try:
# Launch the Boulder server
self._launch_process(['docker-compose', 'up', '--force-recreate'], cwd=instance_path)
# Wait for the ACME CA server to be up.
print('=> Waiting for boulder instance to respond...')
misc.check_until_timeout(
self.acme_xdist['directory_url'], attempts=300)
if not self._dns_server:
# Configure challtestsrv to answer any A record request with ip of the docker host.
response = requests.post(
f'{BOULDER_V2_CHALLTESTSRV_URL}/set-default-ipv4',
json={'ip': '10.77.77.1'},
timeout=10
)
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.wait(MAX_SUBPROCESS_WAIT)
raise
print('=> Finished boulder instance deployment.')
def _prepare_http_proxy(self) -> None:
"""Configure and launch an HTTP proxy"""
print(f'=> Configuring the HTTP proxy on port {self._http_01_port}...')
@@ -177,11 +260,26 @@ class ACMEServer:
self._processes.append(process)
return process
def _register_preterminate_cmd(self, *args: Any, **kwargs: Any) -> None:
self._preterminate_cmds_args.append((args, kwargs))
def _run_preterminate_cmds(self) -> None:
for args, kwargs in self._preterminate_cmds_args:
process = self._launch_process(*args, **kwargs)
process.wait(MAX_SUBPROCESS_WAIT)
# It's unlikely to matter, but let's clear the list of cleanup commands
# once they've been run.
self._preterminate_cmds_args.clear()
def main() -> None:
# pylint: disable=missing-function-docstring
parser = argparse.ArgumentParser(
description='CLI tool to start a local instance of Pebble CA server.')
description='CLI tool to start a local instance of Pebble or Boulder CA server.')
parser.add_argument('--server-type', '-s',
choices=['pebble', 'boulder-v2'], default='pebble',
help='type of CA server to start: can be Pebble or Boulder. '
'Pebble is used if not set.')
parser.add_argument('--dns-server', '-d',
help='specify the DNS server as `IP:PORT` to use by '
'Pebble; if not specified, a local mock DNS server will be used to '
@@ -192,8 +290,8 @@ def main() -> None:
args = parser.parse_args()
acme_server = ACMEServer(
[], http_proxy=False, stdout=True, dns_server=args.dns_server,
http_01_port=args.http_01_port,
args.server_type, [], http_proxy=False, stdout=True,
dns_server=args.dns_server, http_01_port=args.http_01_port,
)
try:

View File

@@ -1,7 +1,10 @@
"""Some useful constants to use throughout certbot-ci integration tests"""
DEFAULT_HTTP_01_PORT = 5002
BOULDER_HTTP_01_PORT = 80
TLS_ALPN_01_PORT = 5001
CHALLTESTSRV_PORT = 8055
BOULDER_V2_CHALLTESTSRV_URL = f'http://10.77.77.77:{CHALLTESTSRV_PORT}'
BOULDER_V2_DIRECTORY_URL = 'http://localhost:4001/directory'
PEBBLE_DIRECTORY_URL = 'https://localhost:14000/dir'
PEBBLE_MANAGEMENT_URL = 'https://localhost:15000'
PEBBLE_CHALLTESTSRV_URL = f'http://localhost:{CHALLTESTSRV_PORT}'

View File

@@ -22,7 +22,7 @@ if sys.version_info >= (3, 9): # pragma: no cover
else: # pragma: no cover
import importlib_resources
BIND_DOCKER_IMAGE = "internetsystemsconsortium/bind9:9.20"
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

View File

@@ -33,6 +33,7 @@ from cryptography.x509 import load_pem_x509_certificate
from OpenSSL import crypto
import requests
from certbot_integration_tests.certbot_tests.context import IntegrationTestsContext
from certbot_integration_tests.utils.constants import PEBBLE_ALTERNATE_ROOTS
from certbot_integration_tests.utils.constants import PEBBLE_MANAGEMENT_URL
@@ -46,8 +47,15 @@ ECDSA_KEY_TYPE = 'ecdsa'
def _suppress_x509_verification_warnings() -> None:
import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
try:
import urllib3
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( # type: ignore[attr-defined]
InsecureRequestWarning)
def check_until_timeout(url: str, attempts: int = 30) -> None:
@@ -124,8 +132,8 @@ def generate_test_file_hooks(config_dir: str, hook_probe: str) -> None:
"""
file_manager = contextlib.ExitStack()
atexit.register(file_manager.close)
hook_path_ref = (importlib_resources.files('certbot_integration_tests').joinpath('assets')
.joinpath('hook.py'))
hook_path_ref = importlib_resources.files('certbot_integration_tests').joinpath(
'assets', 'hook.py')
hook_path = str(file_manager.enter_context(importlib_resources.as_file(hook_path_ref)))
for hook_dir in list_renewal_hooks_dirs(config_dir):
@@ -261,8 +269,9 @@ def load_sample_data_path(workspace: str) -> str:
:returns: the path to the loaded sample data directory
:rtype: str
"""
original_ref = (importlib_resources.files('certbot_integration_tests').joinpath('assets')
.joinpath('sample-config'))
original_ref = importlib_resources.files('certbot_integration_tests').joinpath(
'assets', 'sample-config'
)
with importlib_resources.as_file(original_ref) as original:
copied = os.path.join(workspace, 'sample-config')
shutil.copytree(original, copied, symlinks=True)
@@ -302,12 +311,16 @@ def echo(keyword: str, path: Optional[str] = None) -> str:
os.path.basename(sys.executable), keyword, ' >> "{0}"'.format(path) if path else '')
def get_acme_issuers() -> List[Certificate]:
def get_acme_issuers(context: IntegrationTestsContext) -> List[Certificate]:
"""Gets the list of one or more issuer certificates from the ACME server used by the
context.
:param context: the testing context.
:return: the `list of x509.Certificate` representing the list of issuers.
"""
# TODO: in fact, Boulder has alternate chains in config-next/, just not yet in config/.
if context.acme_server != "pebble":
raise NotImplementedError()
_suppress_x509_verification_warnings()
issuers = []

View File

@@ -1,13 +1,11 @@
# pylint: disable=missing-module-docstring
import atexit
import io
import json
import os
import stat
import sys
import zipfile
from contextlib import ExitStack
from typing import Optional, Tuple
from typing import Tuple
import requests
@@ -19,49 +17,39 @@ if sys.version_info >= (3, 9): # pragma: no cover
else: # pragma: no cover
import importlib_resources
PEBBLE_VERSION = 'v2.5.1'
PEBBLE_VERSION = 'v2.3.1'
def fetch(workspace: str, http_01_port: int = DEFAULT_HTTP_01_PORT) -> Tuple[str, str, str]:
# pylint: disable=missing-function-docstring
suffix = 'linux-amd64' if os.name != 'nt' else 'windows-amd64.exe'
file_manager = ExitStack()
atexit.register(file_manager.close)
pebble_path_ref = importlib_resources.files('certbot_integration_tests') / 'assets'
assets_path = str(file_manager.enter_context(importlib_resources.as_file(pebble_path_ref)))
pebble_path = _fetch_asset('pebble', assets_path)
challtestsrv_path = _fetch_asset('pebble-challtestsrv', assets_path)
pebble_path = _fetch_asset('pebble', suffix, assets_path)
challtestsrv_path = _fetch_asset('pebble-challtestsrv', suffix, assets_path)
pebble_config_path = _build_pebble_config(workspace, http_01_port, assets_path)
return pebble_path, challtestsrv_path, pebble_config_path
def _fetch_asset(asset: str, assets_path: str) -> str:
platform = 'linux-amd64'
base_url = 'https://github.com/letsencrypt/pebble/releases/download'
asset_path = os.path.join(assets_path, f'{asset}_{PEBBLE_VERSION}_{platform}')
def _fetch_asset(asset: str, suffix: str, assets_path: str) -> str:
asset_path = os.path.join(assets_path, '{0}_{1}_{2}'.format(asset, PEBBLE_VERSION, suffix))
if not os.path.exists(asset_path):
asset_url = f'{base_url}/{PEBBLE_VERSION}/{asset}-{platform}.zip'
asset_url = ('https://github.com/letsencrypt/pebble/releases/download/{0}/{1}_{2}'
.format(PEBBLE_VERSION, asset, suffix))
response = requests.get(asset_url, timeout=30)
response.raise_for_status()
asset_data = _unzip_asset(response.content, asset)
if asset_data is None:
raise ValueError(f"zipfile {asset_url} didn't contain file {asset}")
with open(asset_path, 'wb') as file_h:
file_h.write(asset_data)
file_h.write(response.content)
os.chmod(asset_path, os.stat(asset_path).st_mode | stat.S_IEXEC)
return asset_path
def _unzip_asset(zipped_data: bytes, asset_name: str) -> Optional[bytes]:
with zipfile.ZipFile(io.BytesIO(zipped_data)) as zip_file:
for entry in zip_file.filelist:
if not entry.is_dir() and entry.filename.endswith(asset_name):
return zip_file.read(entry)
return None
def _build_pebble_config(workspace: str, http_01_port: int, assets_path: str) -> str:
config_path = os.path.join(workspace, 'pebble-config.json')
with open(config_path, 'w') as file_h:

View File

@@ -19,11 +19,9 @@ install_requires = [
'pywin32>=300 ; sys_platform == "win32"',
'pyyaml',
'pytz>=2019.3',
# requests unvendored its dependencies in version 2.16.0 and this code relies on that for
# calling `urllib3.disable_warnings`.
'requests>=2.16.0',
'requests',
'setuptools',
'types-python-dateutil',
'types-python-dateutil'
]
setup(
@@ -45,7 +43,6 @@ setup(
'Programming Language :: Python :: 3.9',
'Programming Language :: Python :: 3.10',
'Programming Language :: Python :: 3.11',
'Programming Language :: Python :: 3.12',
'Topic :: Internet :: WWW/HTTP',
'Topic :: Security',
],

View File

@@ -75,7 +75,7 @@ def _get_server_root(config: str) -> str:
if os.path.isdir(os.path.join(config, name))]
if len(subdirs) != 1:
raise errors.Error("Malformed configuration directory {0}".format(config))
errors.Error("Malformed configuration directory {0}".format(config))
return os.path.join(config, subdirs[0].rstrip())

View File

@@ -19,6 +19,7 @@
server {
listen 80 default_server;
listen [::]:80 default_server ipv6only=on;
root /usr/share/nginx/html;
index index.html index.htm;

View File

@@ -1,7 +1,7 @@
from setuptools import find_packages
from setuptools import setup
version = '2.12.0.dev0'
version = '2.8.0.dev0'
install_requires = [
'certbot',
@@ -29,7 +29,6 @@ setup(
'Programming Language :: Python :: 3.9',
'Programming Language :: Python :: 3.10',
'Programming Language :: Python :: 3.11',
'Programming Language :: Python :: 3.12',
'Topic :: Internet :: WWW/HTTP',
'Topic :: Security',
],

View File

@@ -14,14 +14,14 @@ build:
# Build documentation in the "docs/" directory with Sphinx
sphinx:
configuration: certbot-dns-cloudflare/docs/conf.py
configuration: docs/conf.py
# You can configure Sphinx to use a different builder, for instance use the dirhtml builder for simpler URLs
# builder: "dirhtml"
# Fail on all warnings to avoid broken references
fail_on_warning: true
# Optionally build your docs in additional formats such as PDF and ePub
formats:
# formats:
- pdf
- epub
@@ -30,4 +30,4 @@ formats:
# See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html
python:
install:
- requirements: certbot-dns-cloudflare/readthedocs.org.requirements.txt
- requirements: ../tools/requirements.txt

View File

@@ -4,12 +4,10 @@ import sys
from setuptools import find_packages
from setuptools import setup
version = '2.12.0.dev0'
version = '2.8.0.dev0'
install_requires = [
# for now, do not upgrade to cloudflare>=2.20 to avoid deprecation warnings and the breaking
# changes in version 3.0. see https://github.com/certbot/certbot/issues/9938
'cloudflare>=1.5.1, <2.20',
'cloudflare>=1.5.1',
'setuptools>=41.6.0',
]
@@ -54,7 +52,6 @@ setup(
'Programming Language :: Python :: 3.9',
'Programming Language :: Python :: 3.10',
'Programming Language :: Python :: 3.11',
'Programming Language :: Python :: 3.12',
'Topic :: Internet :: WWW/HTTP',
'Topic :: Security',
'Topic :: System :: Installation/Setup',

View File

@@ -14,14 +14,14 @@ build:
# Build documentation in the "docs/" directory with Sphinx
sphinx:
configuration: certbot-dns-digitalocean/docs/conf.py
configuration: docs/conf.py
# You can configure Sphinx to use a different builder, for instance use the dirhtml builder for simpler URLs
# builder: "dirhtml"
# Fail on all warnings to avoid broken references
fail_on_warning: true
# Optionally build your docs in additional formats such as PDF and ePub
formats:
# formats:
- pdf
- epub
@@ -30,4 +30,4 @@ formats:
# See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html
python:
install:
- requirements: certbot-dns-digitalocean/readthedocs.org.requirements.txt
- requirements: ../tools/requirements.txt

View File

@@ -4,7 +4,7 @@ import sys
from setuptools import find_packages
from setuptools import setup
version = '2.12.0.dev0'
version = '2.8.0.dev0'
install_requires = [
'python-digitalocean>=1.11', # 1.15.0 or newer is recommended for TTL support
@@ -52,7 +52,6 @@ setup(
'Programming Language :: Python :: 3.9',
'Programming Language :: Python :: 3.10',
'Programming Language :: Python :: 3.11',
'Programming Language :: Python :: 3.12',
'Topic :: Internet :: WWW/HTTP',
'Topic :: Security',
'Topic :: System :: Installation/Setup',

View File

@@ -14,14 +14,14 @@ build:
# Build documentation in the "docs/" directory with Sphinx
sphinx:
configuration: certbot-dns-dnsimple/docs/conf.py
configuration: docs/conf.py
# You can configure Sphinx to use a different builder, for instance use the dirhtml builder for simpler URLs
# builder: "dirhtml"
# Fail on all warnings to avoid broken references
fail_on_warning: true
# Optionally build your docs in additional formats such as PDF and ePub
formats:
# formats:
- pdf
- epub
@@ -30,4 +30,4 @@ formats:
# See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html
python:
install:
- requirements: certbot-dns-dnsimple/readthedocs.org.requirements.txt
- requirements: ../tools/requirements.txt

View File

@@ -4,7 +4,7 @@ import sys
from setuptools import find_packages
from setuptools import setup
version = '2.12.0.dev0'
version = '2.8.0.dev0'
install_requires = [
# This version of lexicon is required to address the problem described in
@@ -54,7 +54,6 @@ setup(
'Programming Language :: Python :: 3.9',
'Programming Language :: Python :: 3.10',
'Programming Language :: Python :: 3.11',
'Programming Language :: Python :: 3.12',
'Topic :: Internet :: WWW/HTTP',
'Topic :: Security',
'Topic :: System :: Installation/Setup',

View File

@@ -14,14 +14,14 @@ build:
# Build documentation in the "docs/" directory with Sphinx
sphinx:
configuration: certbot-dns-dnsmadeeasy/docs/conf.py
configuration: docs/conf.py
# You can configure Sphinx to use a different builder, for instance use the dirhtml builder for simpler URLs
# builder: "dirhtml"
# Fail on all warnings to avoid broken references
fail_on_warning: true
# Optionally build your docs in additional formats such as PDF and ePub
formats:
# formats:
- pdf
- epub
@@ -30,4 +30,4 @@ formats:
# See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html
python:
install:
- requirements: certbot-dns-dnsmadeeasy/readthedocs.org.requirements.txt
- requirements: ../tools/requirements.txt

View File

@@ -4,7 +4,7 @@ import sys
from setuptools import find_packages
from setuptools import setup
version = '2.12.0.dev0'
version = '2.8.0.dev0'
install_requires = [
'dns-lexicon>=3.14.1',
@@ -52,7 +52,6 @@ setup(
'Programming Language :: Python :: 3.9',
'Programming Language :: Python :: 3.10',
'Programming Language :: Python :: 3.11',
'Programming Language :: Python :: 3.12',
'Topic :: Internet :: WWW/HTTP',
'Topic :: Security',
'Topic :: System :: Installation/Setup',

View File

@@ -14,14 +14,14 @@ build:
# Build documentation in the "docs/" directory with Sphinx
sphinx:
configuration: certbot-dns-gehirn/docs/conf.py
configuration: docs/conf.py
# You can configure Sphinx to use a different builder, for instance use the dirhtml builder for simpler URLs
# builder: "dirhtml"
# Fail on all warnings to avoid broken references
fail_on_warning: true
# Optionally build your docs in additional formats such as PDF and ePub
formats:
# formats:
- pdf
- epub
@@ -30,4 +30,4 @@ formats:
# See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html
python:
install:
- requirements: certbot-dns-gehirn/readthedocs.org.requirements.txt
- requirements: ../tools/requirements.txt

View File

@@ -1,6 +1,7 @@
"""Tests for certbot_dns_gehirn._internal.dns_gehirn."""
import sys
import unittest
from unittest import mock
import pytest

View File

@@ -4,7 +4,7 @@ import sys
from setuptools import find_packages
from setuptools import setup
version = '2.12.0.dev0'
version = '2.8.0.dev0'
install_requires = [
'dns-lexicon>=3.14.1',
@@ -52,7 +52,6 @@ setup(
'Programming Language :: Python :: 3.9',
'Programming Language :: Python :: 3.10',
'Programming Language :: Python :: 3.11',
'Programming Language :: Python :: 3.12',
'Topic :: Internet :: WWW/HTTP',
'Topic :: Security',
'Topic :: System :: Installation/Setup',

View File

@@ -14,14 +14,14 @@ build:
# Build documentation in the "docs/" directory with Sphinx
sphinx:
configuration: certbot-dns-google/docs/conf.py
configuration: docs/conf.py
# You can configure Sphinx to use a different builder, for instance use the dirhtml builder for simpler URLs
# builder: "dirhtml"
# Fail on all warnings to avoid broken references
fail_on_warning: true
# Optionally build your docs in additional formats such as PDF and ePub
formats:
# formats:
- pdf
- epub
@@ -30,4 +30,4 @@ formats:
# See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html
python:
install:
- requirements: certbot-dns-google/readthedocs.org.requirements.txt
- requirements: ../tools/requirements.txt

View File

@@ -4,7 +4,7 @@ import sys
from setuptools import find_packages
from setuptools import setup
version = '2.12.0.dev0'
version = '2.8.0.dev0'
install_requires = [
'google-api-python-client>=1.6.5',
@@ -53,7 +53,6 @@ setup(
'Programming Language :: Python :: 3.9',
'Programming Language :: Python :: 3.10',
'Programming Language :: Python :: 3.11',
'Programming Language :: Python :: 3.12',
'Topic :: Internet :: WWW/HTTP',
'Topic :: Security',
'Topic :: System :: Installation/Setup',

View File

@@ -14,14 +14,14 @@ build:
# Build documentation in the "docs/" directory with Sphinx
sphinx:
configuration: certbot-dns-linode/docs/conf.py
configuration: docs/conf.py
# You can configure Sphinx to use a different builder, for instance use the dirhtml builder for simpler URLs
# builder: "dirhtml"
# Fail on all warnings to avoid broken references
fail_on_warning: true
# Optionally build your docs in additional formats such as PDF and ePub
formats:
# formats:
- pdf
- epub
@@ -30,4 +30,4 @@ formats:
# See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html
python:
install:
- requirements: certbot-dns-linode/readthedocs.org.requirements.txt
- requirements: ../tools/requirements.txt

View File

@@ -4,7 +4,7 @@ import sys
from setuptools import find_packages
from setuptools import setup
version = '2.12.0.dev0'
version = '2.8.0.dev0'
install_requires = [
'dns-lexicon>=3.14.1',
@@ -52,7 +52,6 @@ setup(
'Programming Language :: Python :: 3.9',
'Programming Language :: Python :: 3.10',
'Programming Language :: Python :: 3.11',
'Programming Language :: Python :: 3.12',
'Topic :: Internet :: WWW/HTTP',
'Topic :: Security',
'Topic :: System :: Installation/Setup',

View File

@@ -14,14 +14,14 @@ build:
# Build documentation in the "docs/" directory with Sphinx
sphinx:
configuration: certbot-dns-luadns/docs/conf.py
configuration: docs/conf.py
# You can configure Sphinx to use a different builder, for instance use the dirhtml builder for simpler URLs
# builder: "dirhtml"
# Fail on all warnings to avoid broken references
fail_on_warning: true
# Optionally build your docs in additional formats such as PDF and ePub
formats:
# formats:
- pdf
- epub
@@ -30,4 +30,4 @@ formats:
# See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html
python:
install:
- requirements: certbot-dns-luadns/readthedocs.org.requirements.txt
- requirements: ../tools/requirements.txt

View File

@@ -4,7 +4,7 @@ import sys
from setuptools import find_packages
from setuptools import setup
version = '2.12.0.dev0'
version = '2.8.0.dev0'
install_requires = [
'dns-lexicon>=3.14.1',
@@ -52,7 +52,6 @@ setup(
'Programming Language :: Python :: 3.9',
'Programming Language :: Python :: 3.10',
'Programming Language :: Python :: 3.11',
'Programming Language :: Python :: 3.12',
'Topic :: Internet :: WWW/HTTP',
'Topic :: Security',
'Topic :: System :: Installation/Setup',

View File

@@ -14,14 +14,14 @@ build:
# Build documentation in the "docs/" directory with Sphinx
sphinx:
configuration: certbot-dns-nsone/docs/conf.py
configuration: docs/conf.py
# You can configure Sphinx to use a different builder, for instance use the dirhtml builder for simpler URLs
# builder: "dirhtml"
# Fail on all warnings to avoid broken references
fail_on_warning: true
# Optionally build your docs in additional formats such as PDF and ePub
formats:
# formats:
- pdf
- epub
@@ -30,4 +30,4 @@ formats:
# See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html
python:
install:
- requirements: certbot-dns-nsone/readthedocs.org.requirements.txt
- requirements: ../tools/requirements.txt

View File

@@ -4,7 +4,7 @@ import sys
from setuptools import find_packages
from setuptools import setup
version = '2.12.0.dev0'
version = '2.8.0.dev0'
install_requires = [
'dns-lexicon>=3.14.1',
@@ -52,7 +52,6 @@ setup(
'Programming Language :: Python :: 3.9',
'Programming Language :: Python :: 3.10',
'Programming Language :: Python :: 3.11',
'Programming Language :: Python :: 3.12',
'Topic :: Internet :: WWW/HTTP',
'Topic :: Security',
'Topic :: System :: Installation/Setup',

View File

@@ -14,14 +14,14 @@ build:
# Build documentation in the "docs/" directory with Sphinx
sphinx:
configuration: certbot-dns-ovh/docs/conf.py
configuration: docs/conf.py
# You can configure Sphinx to use a different builder, for instance use the dirhtml builder for simpler URLs
# builder: "dirhtml"
# Fail on all warnings to avoid broken references
fail_on_warning: true
# Optionally build your docs in additional formats such as PDF and ePub
formats:
# formats:
- pdf
- epub
@@ -30,4 +30,4 @@ formats:
# See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html
python:
install:
- requirements: certbot-dns-ovh/readthedocs.org.requirements.txt
- requirements: ../tools/requirements.txt

View File

@@ -22,7 +22,6 @@ class AuthenticatorTest(test_util.TempDirTestCase,
DOMAIN_NOT_FOUND = Exception('Domain example.com not found')
LOGIN_ERROR = HTTPError('403 Client Error: Forbidden for url: https://eu.api.ovh.com/1.0/...', response=Response())
def setUp(self):
super().setUp()

View File

@@ -4,10 +4,10 @@ import sys
from setuptools import find_packages
from setuptools import setup
version = '2.12.0.dev0'
version = '2.8.0.dev0'
install_requires = [
'dns-lexicon>=3.15.1',
'dns-lexicon>=3.14.1',
'setuptools>=41.6.0',
]
@@ -52,7 +52,6 @@ setup(
'Programming Language :: Python :: 3.9',
'Programming Language :: Python :: 3.10',
'Programming Language :: Python :: 3.11',
'Programming Language :: Python :: 3.12',
'Topic :: Internet :: WWW/HTTP',
'Topic :: Security',
'Topic :: System :: Installation/Setup',

View File

@@ -14,14 +14,14 @@ build:
# Build documentation in the "docs/" directory with Sphinx
sphinx:
configuration: certbot-dns-rfc2136/docs/conf.py
configuration: docs/conf.py
# You can configure Sphinx to use a different builder, for instance use the dirhtml builder for simpler URLs
# builder: "dirhtml"
# Fail on all warnings to avoid broken references
fail_on_warning: true
# Optionally build your docs in additional formats such as PDF and ePub
formats:
# formats:
- pdf
- epub
@@ -30,4 +30,4 @@ formats:
# See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html
python:
install:
- requirements: certbot-dns-rfc2136/readthedocs.org.requirements.txt
- requirements: ../tools/requirements.txt

View File

@@ -4,7 +4,7 @@ import sys
from setuptools import find_packages
from setuptools import setup
version = '2.12.0.dev0'
version = '2.8.0.dev0'
install_requires = [
'dnspython>=1.15.0',
@@ -52,7 +52,6 @@ setup(
'Programming Language :: Python :: 3.9',
'Programming Language :: Python :: 3.10',
'Programming Language :: Python :: 3.11',
'Programming Language :: Python :: 3.12',
'Topic :: Internet :: WWW/HTTP',
'Topic :: Security',
'Topic :: System :: Installation/Setup',

View File

@@ -14,14 +14,14 @@ build:
# Build documentation in the "docs/" directory with Sphinx
sphinx:
configuration: certbot-dns-route53/docs/conf.py
configuration: docs/conf.py
# You can configure Sphinx to use a different builder, for instance use the dirhtml builder for simpler URLs
# builder: "dirhtml"
# Fail on all warnings to avoid broken references
fail_on_warning: true
# Optionally build your docs in additional formats such as PDF and ePub
formats:
# formats:
- pdf
- epub
@@ -30,4 +30,4 @@ formats:
# See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html
python:
install:
- requirements: certbot-dns-route53/readthedocs.org.requirements.txt
- requirements: ../tools/requirements.txt

View File

@@ -4,7 +4,7 @@ import sys
from setuptools import find_packages
from setuptools import setup
version = '2.12.0.dev0'
version = '2.8.0.dev0'
install_requires = [
'boto3>=1.15.15',
@@ -52,7 +52,6 @@ setup(
'Programming Language :: Python :: 3.9',
'Programming Language :: Python :: 3.10',
'Programming Language :: Python :: 3.11',
'Programming Language :: Python :: 3.12',
'Topic :: Internet :: WWW/HTTP',
'Topic :: Security',
'Topic :: System :: Installation/Setup',

View File

@@ -14,14 +14,14 @@ build:
# Build documentation in the "docs/" directory with Sphinx
sphinx:
configuration: certbot-dns-sakuracloud/docs/conf.py
configuration: docs/conf.py
# You can configure Sphinx to use a different builder, for instance use the dirhtml builder for simpler URLs
# builder: "dirhtml"
# Fail on all warnings to avoid broken references
fail_on_warning: true
# Optionally build your docs in additional formats such as PDF and ePub
formats:
# formats:
- pdf
- epub
@@ -30,4 +30,4 @@ formats:
# See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html
python:
install:
- requirements: certbot-dns-sakuracloud/readthedocs.org.requirements.txt
- requirements: ../tools/requirements.txt

View File

@@ -4,7 +4,7 @@ import sys
from setuptools import find_packages
from setuptools import setup
version = '2.12.0.dev0'
version = '2.8.0.dev0'
install_requires = [
'dns-lexicon>=3.14.1',
@@ -52,7 +52,6 @@ setup(
'Programming Language :: Python :: 3.9',
'Programming Language :: Python :: 3.10',
'Programming Language :: Python :: 3.11',
'Programming Language :: Python :: 3.12',
'Topic :: Internet :: WWW/HTTP',
'Topic :: Security',
'Topic :: System :: Installation/Setup',

View File

@@ -18,6 +18,7 @@ from typing import Mapping
from typing import Optional
from typing import Sequence
from typing import Set
from typing import Text
from typing import Tuple
from typing import Type
from typing import Union
@@ -171,8 +172,8 @@ class NginxConfigurator(common.Configurator):
file_manager = ExitStack()
atexit.register(file_manager.close)
ref = (importlib_resources.files("certbot_nginx").joinpath("_internal")
.joinpath("tls_configs").joinpath(config_filename))
ref = importlib_resources.files("certbot_nginx").joinpath(
"_internal", "tls_configs", config_filename)
return str(file_manager.enter_context(importlib_resources.as_file(ref)))
@@ -701,7 +702,7 @@ class NginxConfigurator(common.Configurator):
# TODO: generate only once
tmp_dir = os.path.join(self.config.work_dir, "snakeoil")
le_key = crypto_util.generate_key(
key_size=2048, key_dir=tmp_dir, keyname="key.pem",
key_size=1024, key_dir=tmp_dir, keyname="key.pem",
strict_permissions=self.config.strict_permissions)
assert le_key.file is not None
key = OpenSSL.crypto.load_privatekey(
@@ -1274,7 +1275,7 @@ def nginx_restart(nginx_ctl: str, nginx_conf: str, sleep_duration: int) -> None:
"""
try:
reload_output: str = ""
reload_output: Text = ""
with tempfile.TemporaryFile() as out:
proc = subprocess.run([nginx_ctl, "-c", nginx_conf, "-s", "reload"],
env=util.env_no_snap_for_external_calls(),

View File

@@ -294,7 +294,7 @@ def dumps(blocks: UnspacedList) -> str:
"""Dump to a Unicode string.
:param UnspacedList blocks: The parsed tree
:rtype: str
:rtype: six.text_type
"""
return str(RawNginxDumper(blocks.spaced))

View File

@@ -1,7 +1,7 @@
from setuptools import find_packages
from setuptools import setup
version = '2.12.0.dev0'
version = '2.8.0.dev0'
install_requires = [
# We specify the minimum acme and certbot version as the current plugin
@@ -41,7 +41,6 @@ setup(
'Programming Language :: Python :: 3.9',
'Programming Language :: Python :: 3.10',
'Programming Language :: Python :: 3.11',
'Programming Language :: Python :: 3.12',
'Topic :: Internet :: WWW/HTTP',
'Topic :: Security',
'Topic :: System :: Installation/Setup',

View File

@@ -14,14 +14,14 @@ build:
# Build documentation in the "docs/" directory with Sphinx
sphinx:
configuration: certbot/docs/conf.py
configuration: docs/conf.py
# You can configure Sphinx to use a different builder, for instance use the dirhtml builder for simpler URLs
# builder: "dirhtml"
# Fail on all warnings to avoid broken references
fail_on_warning: true
# Optionally build your docs in additional formats such as PDF and ePub
formats:
# formats:
- pdf
- epub
@@ -30,4 +30,4 @@ formats:
# See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html
python:
install:
- requirements: certbot/readthedocs.org.requirements.txt
- requirements: ../tools/requirements.txt

View File

@@ -2,7 +2,7 @@
Certbot adheres to [Semantic Versioning](https://semver.org/).
## 3.0.0 - master
## 2.8.0 - master
### Added
@@ -10,129 +10,10 @@ Certbot adheres to [Semantic Versioning](https://semver.org/).
### Changed
* The update_symlinks command was removed.
* The `csr_dir` and `key_dir` attributes on
`certbot.configuration.NamespaceConfig` were removed.
### Fixed
*
More details about these changes can be found on our GitHub repo.
## 2.11.0 - 2024-06-05
### Added
*
### Changed
* In anticipation of backwards incompatible changes, certbot-dns-cloudflare now
requires less than version 2.20 of Cloudflare's python library.
### Fixed
* Fixed a bug in Certbot where a CSR's SANs did not always follow the order of
the domain names that the user requested interactively. In some cases, the
resulting cert's common name might seem picked up randomly from the SANs
when it should be the first item the user had in mind.
More details about these changes can be found on our GitHub repo.
## 2.10.0 - 2024-04-02
### Added
* The Python source packages which we upload to [PyPI](https://pypi.org/) are
now also being uploaded to
[our releases on GitHub](https://github.com/certbot/certbot/releases) where
we now also include a SHA256SUMS checksum file and a PGP signature for that
file.
### Changed
* We no longer publish our beta Windows installer as was originally announced
[here](https://community.letsencrypt.org/t/certbot-discontinuing-windows-beta-support-in-2024/208101).
### Fixed
*
More details about these changes can be found on our GitHub repo.
## 2.9.0 - 2024-02-08
### Added
* Support for Python 3.12 was added.
### Changed
*
### Fixed
* Updates `joinpath` syntax to only use one addition per call, because the multiple inputs
version was causing mypy errors on Python 3.10.
* Makes the `reconfigure` verb actually use the staging server for the dry run to check the new
configuration.
More details about these changes can be found on our GitHub repo.
## 2.8.0 - 2023-12-05
### Added
* Added support for [Alpine Linux](https://www.alpinelinux.org) distribution when is used the apache plugin
### Changed
* Support for Python 3.7 was removed.
### Fixed
* Stop using the deprecated `pkg_resources` API included in `setuptools`.
More details about these changes can be found on our GitHub repo.
## 2.7.4 - 2023-11-01
### Fixed
* Fixed a bug introduced in version 2.7.0 that caused interactively entered
webroot plugin values to not be saved for renewal.
* Fixed a bug introduced in version 2.7.0 of our Lexicon based DNS plugins that
caused them to fail to find the DNS zone that needs to be modified in some
cases.
More details about these changes can be found on our GitHub repo.
## 2.7.3 - 2023-10-24
### Fixed
* Fixed a bug where arguments with contained spaces weren't being handled correctly
* Fixed a bug that caused the ACME account to not be properly restored on
renewal causing problems in setups where the user had multiple accounts with
the same ACME server.
More details about these changes can be found on our GitHub repo.
## 2.7.2 - 2023-10-19
### Fixed
* `certbot-dns-ovh` plugin now requires `lexicon>=3.15.1` to ensure a consistent behavior with OVH APIs.
* Fixed a bug where argument sources weren't correctly detected in abbreviated
arguments, short arguments, and some other circumstances
More details about these changes can be found on our GitHub repo.
## 2.7.1 - 2023-10-10
### Fixed
* Fixed a bug that broke the DNS plugin for DNSimple that was introduced in
version 2.7.0 of the plugin.
* Correctly specified the new minimum version of the ConfigArgParse package

View File

@@ -1,4 +1,4 @@
"""Certbot client."""
# version number like 1.2.3a0, must have at least 2 parts, like 1.2
__version__ = '2.12.0.dev0'
__version__ = '2.8.0.dev0'

View File

@@ -30,6 +30,22 @@ logger = logging.getLogger(__name__)
###################
def update_live_symlinks(config: configuration.NamespaceConfig) -> None:
"""Update the certificate file family symlinks to use archive_dir.
Use the information in the config file to make symlinks point to
the correct archive directory.
.. note:: This assumes that the installation is using a Reverter object.
:param config: Configuration.
:type config: :class:`certbot._internal.configuration.NamespaceConfig`
"""
for renewal_file in storage.renewal_conf_files(config):
storage.RenewableCert(renewal_file, config, update_symlinks=True)
def rename_lineage(config: configuration.NamespaceConfig) -> None:
"""Rename the specified lineage to the new name.

View File

@@ -36,7 +36,6 @@ from certbot._internal.cli.cli_utils import HelpfulArgumentGroup
from certbot._internal.cli.cli_utils import nonnegative_int
from certbot._internal.cli.cli_utils import parse_preferred_challenges
from certbot._internal.cli.cli_utils import read_file
from certbot._internal.cli.cli_utils import set_test_server_options
from certbot._internal.cli.group_adder import _add_all_groups
from certbot._internal.cli.helpful import HelpfulArgumentParser
from certbot._internal.cli.paths_parser import _paths_parser

View File

@@ -1,7 +1,6 @@
"""Certbot command line util function"""
import argparse
import copy
import glob
import inspect
from typing import Any
from typing import Iterable
@@ -251,48 +250,3 @@ def nonnegative_int(value: str) -> int:
if int_value < 0:
raise argparse.ArgumentTypeError("value must be non-negative")
return int_value
def set_test_server_options(verb: str, config: configuration.NamespaceConfig) -> None:
"""Updates server, break_my_certs, staging, tos, and
register_unsafely_without_email in config as necessary to prepare
to use the test server.
We have --staging/--dry-run; perform sanity check and set config.server
:param str verb: subcommand called
:param config: parsed command line arguments
:type config: configuration.NamespaceConfig
:raises errors.Error: if non-default server is used and --staging is set
:raises errors.Error: if inapplicable verb is used and --dry-run is set
"""
# Flag combinations should produce these results:
# | --staging | --dry-run |
# ------------------------------------------------------------
# | --server acme-v02 | Use staging | Use staging |
# | --server acme-staging-v02 | Use staging | Use staging |
# | --server <other> | Conflict error | Use <other> |
default_servers = (flag_default("server"), constants.STAGING_URI)
if config.staging and config.server not in default_servers:
raise errors.Error("--server value conflicts with --staging")
if config.server == flag_default("server"):
config.server = constants.STAGING_URI
# If the account has already been loaded (such as by calling reconstitute before this),
# clear it so that we don't try to use the prod account on the staging server.
config.account = None
if config.dry_run:
if verb not in ["certonly", "renew", "reconfigure"]:
raise errors.Error("--dry-run currently only works with the "
"'certonly' or 'renew' subcommands (%r)" % verb)
config.break_my_certs = config.staging = True
if glob.glob(os.path.join(config.config_dir, constants.ACCOUNTS_DIR, "*")):
# The user has a prod account, but might not have a staging
# one; we don't want to start trying to perform interactive registration
config.tos = True
config.register_unsafely_without_email = True

View File

@@ -2,6 +2,7 @@
import argparse
import functools
import glob
import sys
from typing import Any
from typing import Dict
@@ -25,11 +26,11 @@ from certbot._internal.cli.cli_utils import add_domains
from certbot._internal.cli.cli_utils import CustomHelpFormatter
from certbot._internal.cli.cli_utils import flag_default
from certbot._internal.cli.cli_utils import HelpfulArgumentGroup
from certbot._internal.cli.cli_utils import set_test_server_options
from certbot._internal.cli.verb_help import VERB_HELP
from certbot._internal.cli.verb_help import VERB_HELP_MAP
from certbot._internal.display import obj as display_obj
from certbot._internal.plugins import disco
from certbot.compat import os
from certbot.configuration import ArgumentSource
from certbot.configuration import NamespaceConfig
@@ -58,6 +59,7 @@ class HelpfulArgumentParser:
"revoke": main.revoke,
"rollback": main.rollback,
"everything": main.run,
"update_symlinks": main.update_symlinks,
"certificates": main.certificates,
"delete": main.delete,
"enhance": main.enhance,
@@ -163,7 +165,6 @@ class HelpfulArgumentParser:
def remove_config_file_domains_for_renewal(self, config: NamespaceConfig) -> None:
"""Make "certbot renew" safe if domains are set in cli.ini."""
# Works around https://github.com/certbot/certbot/issues/4096
assert config.argument_sources is not None
if (config.argument_sources['domains'] == ArgumentSource.CONFIG_FILE and
self.verb == "renew"):
config.domains = []
@@ -186,12 +187,10 @@ class HelpfulArgumentParser:
# 2. config files
# 3. env vars (shouldn't be any)
# 4. command line
def update_result(settings_dict: Dict[str, Tuple[configargparse.Action, str]],
source: ArgumentSource) -> None:
actions = [self._find_action_for_arg(arg) if action is None else action
for arg, (action, _) in settings_dict.items()]
result.update({ action.dest: source for action in actions })
actions = [action for _, (action, _) in settings_dict.items()]
result.update({ action.dest: source for action in actions})
# config file sources look like "config_file|<name of file>"
for source_key in source_to_settings_dict:
@@ -204,61 +203,17 @@ class HelpfulArgumentParser:
if 'command_line' in source_to_settings_dict:
settings_dict: Dict[str, Tuple[None, List[str]]]
settings_dict = source_to_settings_dict['command_line'] # type: ignore
(_, unprocessed_args) = settings_dict['']
args = []
for arg in unprocessed_args:
# ignore non-arguments
if not arg.startswith('-'):
continue
# special case for config file argument, which we don't have an action for
if arg in ['-c', '--config']:
result['config_dir'] = ArgumentSource.COMMAND_LINE
continue
if '=' in arg:
arg = arg.split('=')[0]
elif ' ' in arg:
arg = arg.split(' ')[0]
if arg.startswith('--'):
args.append(arg)
# for short args (ones that start with a single hyphen), handle
# the case of multiple short args together, e.g. "-tvm"
else:
for short_arg in arg[1:]:
args.append(f"-{short_arg}")
(_, args) = settings_dict['']
args = [arg for arg in args if arg.startswith('-')]
for arg in args:
# find the action corresponding to this arg
action = self._find_action_for_arg(arg)
result[action.dest] = ArgumentSource.COMMAND_LINE
for action in self.actions:
if arg in action.option_strings:
result[action.dest] = ArgumentSource.COMMAND_LINE
continue
return result
def _find_action_for_arg(self, arg: str) -> configargparse.Action:
# Finds a configargparse Action which matches the given arg, where arg
# can either be preceded by hyphens (as on the command line) or not (as
# in config files)
# if the argument doesn't have leading hypens, prefix it so it can be
# compared directly w/ action option strings
if arg[0] != '-':
arg = '--' + arg
# first, check for exact matches
for action in self.actions:
if arg in action.option_strings:
return action
# now check for abbreviated (i.e. prefix) matches
for action in self.actions:
for option_string in action.option_strings:
if option_string.startswith(arg):
return action
raise AssertionError(f"Action corresponding to argument {arg} is None")
def parse_args(self) -> NamespaceConfig:
"""Parses command line arguments and returns the result.
@@ -316,10 +271,33 @@ class HelpfulArgumentParser:
return config
def set_test_server(self, config: NamespaceConfig) -> None:
"""Updates server, break_my_certs, staging, tos, and
register_unsafely_without_email in config as necessary to prepare
to use the test server."""
return set_test_server_options(self.verb, config)
"""We have --staging/--dry-run; perform sanity check and set config.server"""
# Flag combinations should produce these results:
# | --staging | --dry-run |
# ------------------------------------------------------------
# | --server acme-v02 | Use staging | Use staging |
# | --server acme-staging-v02 | Use staging | Use staging |
# | --server <other> | Conflict error | Use <other> |
default_servers = (flag_default("server"), constants.STAGING_URI)
if config.staging and config.server not in default_servers:
raise errors.Error("--server value conflicts with --staging")
if config.server == flag_default("server"):
config.server = constants.STAGING_URI
if config.dry_run:
if self.verb not in ["certonly", "renew"]:
raise errors.Error("--dry-run currently only works with the "
"'certonly' or 'renew' subcommands (%r)" % self.verb)
config.break_my_certs = config.staging = True
if glob.glob(os.path.join(config.config_dir, constants.ACCOUNTS_DIR, "*")):
# The user has a prod account, but might not have a staging
# one; we don't want to start trying to perform interactive registration
config.tos = True
config.register_unsafely_without_email = True
def handle_csr(self, config: NamespaceConfig) -> None:
"""Process a --csr flag."""

View File

@@ -21,7 +21,7 @@ SETUPTOOLS_PLUGINS_ENTRY_POINT = "certbot.plugins"
OLD_SETUPTOOLS_PLUGINS_ENTRY_POINT = "letsencrypt.plugins"
"""Plugins Setuptools entry point before rename."""
CLI_DEFAULTS: Dict[str, Any] = dict( # pylint: disable=use-dict-literal
CLI_DEFAULTS: Dict[str, Any] = dict( # noqa
config_files=[
os.path.join(misc.get_default_folder('config'), 'cli.ini'),
# https://freedesktop.org/wiki/Software/xdg-user-dirs/
@@ -182,6 +182,9 @@ BACKUP_DIR = "backups"
"""Directory (relative to `certbot.configuration.NamespaceConfig.work_dir`)
where backups are kept."""
CSR_DIR = "csr"
"""See `certbot.configuration.NamespaceConfig.csr_dir`."""
IN_PROGRESS_DIR = "IN_PROGRESS"
"""Directory used before a permanent checkpoint is finalized (relative to
`certbot.configuration.NamespaceConfig.work_dir`)."""

View File

@@ -328,9 +328,8 @@ class FileDisplay:
except ValueError:
return []
# Remove duplicates. dict is used instead of set, since dict perserves
# insertion order as of Python 3.7
indices_int = list(dict.fromkeys(indices_int).keys())
# Remove duplicates
indices_int = list(set(indices_int))
# Check all input is within range
for index in indices_int:

View File

@@ -15,6 +15,7 @@ from typing import Optional
from typing import Tuple
from typing import TypeVar
from typing import Union
import warnings
import configobj
import josepy as jose
@@ -1265,6 +1266,27 @@ def rollback(config: configuration.NamespaceConfig, plugins: plugins_disco.Plugi
client.rollback(config.installer, config.checkpoints, config, plugins)
def update_symlinks(config: configuration.NamespaceConfig,
unused_plugins: plugins_disco.PluginsRegistry) -> None:
"""Update the certificate file family symlinks
Use the information in the config file to make symlinks point to
the correct archive directory.
:param config: Configuration object
:type config: configuration.NamespaceConfig
:param unused_plugins: List of plugins (deprecated)
:type unused_plugins: plugins_disco.PluginsRegistry
:returns: `None`
:rtype: None
"""
warnings.warn("update_symlinks is deprecated and will be removed", PendingDeprecationWarning)
cert_manager.update_live_symlinks(config)
def rename(config: configuration.NamespaceConfig,
unused_plugins: plugins_disco.PluginsRegistry) -> None:
"""Rename a certificate
@@ -1705,8 +1727,10 @@ def reconfigure(config: configuration.NamespaceConfig,
# to say nothing of the difficulty in explaining what exactly this subcommand can modify
# To make sure that the requested changes work, we're going to do a dry run, and only save
# upon success. First, modify the config as the user requested.
# To make sure that the requested changes work, do a dry run. While setting up the dry run,
# we will set all the needed fields in config, which will then be saved upon success.
config.dry_run = True
if not config.certname:
certname_question = "Which certificate would you like to reconfigure?"
config.certname = cert_manager.get_certnames(
@@ -1748,44 +1772,17 @@ def reconfigure(config: configuration.NamespaceConfig,
if not renewal_candidate:
raise errors.ConfigurationError("Could not load certificate. See logs for errors.")
renewalparams = orig_renewal_conf['renewalparams']
# If server was set but hasn't changed and no account is loaded,
# load the old account because reconstitute won't have
if lineage_config.set_by_user('server') and lineage_config.server == renewalparams['server']\
and lineage_config.account is None:
lineage_config.account = renewalparams['account']
for param in ('account', 'server',):
if getattr(lineage_config, param) != renewalparams.get(param):
msg = ("Using reconfigure to change the ACME account or server is not supported. "
"If you would like to do so, use renew with the --force-renewal flag instead "
"of reconfigure. Note that doing so will count against any rate limits. For "
"more information on this method, see "
"https://certbot.org/renew-reconfiguration")
raise errors.ConfigurationError(msg)
# this is where lineage_config gets fully filled out (e.g. --apache will set auth and installer)
installer, auth = plug_sel.choose_configurator_plugins(lineage_config, plugins, "certonly")
# make a deep copy of lineage_config because we're about to modify it for a test dry run
dry_run_lineage_config = copy.deepcopy(lineage_config)
# we also set noninteractive_mode to more accurately simulate renewal (since `certbot renew`
# implies noninteractive mode) and to avoid prompting the user as changes made to
# dry_run_lineage_config beyond this point will not be applied to the original lineage_config
dry_run_lineage_config.noninteractive_mode = True
dry_run_lineage_config.dry_run = True
cli.set_test_server_options("reconfigure", dry_run_lineage_config)
le_client = _init_le_client(dry_run_lineage_config, auth, installer)
le_client = _init_le_client(lineage_config, auth, installer)
# renews cert as dry run to test that the new values are ok
# at this point, renewal_candidate.configuration has the old values, but will use
# the values from lineage_config when doing the dry run
_get_and_save_cert(le_client, dry_run_lineage_config, certname=certname,
_get_and_save_cert(le_client, lineage_config, certname=certname,
lineage=renewal_candidate)
# this function will update lineage.configuration with the new values, and save it to disk
# use the pre-dry-run version
renewal_candidate.save_new_config_values(lineage_config)
_report_reconfigure_results(renewal_file, orig_renewal_conf)

View File

@@ -207,7 +207,6 @@ class PluginsRegistry(Mapping):
plugin2_dist = other_ep.entry_point.dist
plugin1 = plugin1_dist.name.lower() if plugin1_dist else "unknown"
plugin2 = plugin2_dist.name.lower() if plugin2_dist else "unknown"
# pylint: disable=broad-exception-raised
raise Exception("Duplicate plugin name {0} from {1} and {2}.".format(
plugin_ep.name, plugin1, plugin2))
if issubclass(plugin_ep.plugin_cls, interfaces.Plugin):

View File

@@ -58,7 +58,7 @@ class Authenticator(common.Plugin, interfaces.Authenticator):
description = """\
Saves the necessary validation files to a .well-known/acme-challenge/ directory within the \
nominated webroot path. A separate HTTP server must be running and serving files from the \
nominated webroot path. A seperate HTTP server must be running and serving files from the \
webroot path. HTTP challenge only (wildcards not supported)."""
MORE_INFO = """\

View File

@@ -194,7 +194,6 @@ def restore_required_config_elements(config: configuration.NamespaceConfig,
"""
updated_values = {}
required_items = itertools.chain(
(("pref_challs", _restore_pref_challs),),
zip(BOOL_CONFIG_ITEMS, itertools.repeat(_restore_bool)),
@@ -203,9 +202,7 @@ def restore_required_config_elements(config: configuration.NamespaceConfig,
for item_name, restore_func in required_items:
if item_name in renewalparams and not config.set_by_user(item_name):
value = restore_func(item_name, renewalparams[item_name])
updated_values[item_name] = value
for key, value in updated_values.items():
setattr(config, key, value)
setattr(config, item_name, value)
def _remove_deprecated_config_elements(renewalparams: Mapping[str, Any]) -> Dict[str, Any]:

View File

@@ -4,10 +4,8 @@ import socket
from typing import Iterable
from typing import List
from typing import Optional
from typing import Tuple
from typing import Union
from requests import PreparedRequest, Session
from requests import Session
from requests.adapters import HTTPAdapter
from requests.exceptions import HTTPError
from requests.exceptions import RequestException
@@ -71,7 +69,7 @@ def prepare_env(cli_args: List[str]) -> List[str]:
raise e
data = response.json()
connections = ['/snap/{0}/current/lib/python3.12/site-packages/'.format(item['slot']['snap'])
connections = ['/snap/{0}/current/lib/python3.8/site-packages/'.format(item['slot']['snap'])
for item in data.get('result', {}).get('established', [])
if item.get('plug', {}).get('plug') == 'plugin'
and item.get('plug-attrs', {}).get('content') == 'certbot-1']
@@ -102,21 +100,6 @@ class _SnapdConnectionPool(HTTPConnectionPool):
class _SnapdAdapter(HTTPAdapter):
# get_connection is used with versions of requests before 2.32.2 and
# get_connection_with_tls_context is used instead in versions after that. as of
# writing this, Certbot in EPEL 9 is still seeing updates and they have requests 2.25.1 so to
# help out those packagers while ensuring this code works reliably, we offer custom versions of
# both functions for now. when certbot does declare a dependency on requests>=2.32.2 in its
# setup.py files, get_connection can be deleted
def get_connection(self, url: str,
proxies: Optional[Iterable[str]] = None) -> _SnapdConnectionPool:
return _SnapdConnectionPool()
def get_connection_with_tls_context(self, request: PreparedRequest,
verify: bool,
proxies: Optional[Iterable[str]] = None,
cert: Optional[Union[str, Tuple[str,str]]] = None
) -> _SnapdConnectionPool:
"""Required method for creating a new connection pool. Simply return our
shim that forces a UNIX socket connection to snapd."""
return _SnapdConnectionPool()

View File

@@ -455,7 +455,8 @@ class RenewableCert(interfaces.RenewableCert):
renewal configuration file and/or systemwide defaults.
"""
def __init__(self, config_filename: str, cli_config: configuration.NamespaceConfig) -> None:
def __init__(self, config_filename: str, cli_config: configuration.NamespaceConfig,
update_symlinks: bool = False) -> None:
"""Instantiate a RenewableCert object from an existing lineage.
:param str config_filename: the path to the renewal config file
@@ -504,6 +505,8 @@ class RenewableCert(interfaces.RenewableCert):
self.live_dir = os.path.dirname(self.cert)
self._fix_symlinks()
if update_symlinks:
self._update_symlinks()
self._check_symlinks()
@property
@@ -590,6 +593,17 @@ class RenewableCert(interfaces.RenewableCert):
raise errors.CertStorageError("target {0} of symlink {1} does "
"not exist".format(target, link))
def _update_symlinks(self) -> None:
"""Updates symlinks to use archive_dir"""
for kind in ALL_FOUR:
link = getattr(self, kind)
previous_link = get_link_target(link)
new_link = os.path.join(self.relative_archive_dir(link),
os.path.basename(previous_link))
os.unlink(link)
os.symlink(new_link, link)
def _consistent(self) -> bool:
"""Are the files associated with this lineage self-consistent?
@@ -622,7 +636,10 @@ class RenewableCert(interfaces.RenewableCert):
"cert lineage's directory within the "
"official archive directory. Link: %s, "
"target directory: %s, "
"archive directory: %s.",
"archive directory: %s. If you've specified "
"the archive directory in the renewal configuration "
"file, you may need to update links by running "
"certbot update_symlinks.",
link, os.path.dirname(target), self.archive_dir)
return False

View File

@@ -65,6 +65,44 @@ class BaseCertManagerTest(test_util.ConfigTestCase):
return config_file
class UpdateLiveSymlinksTest(BaseCertManagerTest):
"""Tests for certbot._internal.cert_manager.update_live_symlinks
"""
def test_update_live_symlinks(self):
"""Test update_live_symlinks"""
# create files with incorrect symlinks
from certbot._internal import cert_manager
archive_paths = {}
for domain in self.domains:
custom_archive = self.domains[domain]
if custom_archive is not None:
archive_dir_path = custom_archive
else:
archive_dir_path = os.path.join(self.config.default_archive_dir, domain)
archive_paths[domain] = {kind:
os.path.join(archive_dir_path, kind + "1.pem") for kind in ALL_FOUR}
for kind in ALL_FOUR:
live_path = self.config_files[domain][kind]
archive_path = archive_paths[domain][kind]
open(archive_path, 'a').close()
# path is incorrect but base must be correct
os.symlink(os.path.join(self.config.config_dir, kind + "1.pem"), live_path)
# run update symlinks
cert_manager.update_live_symlinks(self.config)
# check that symlinks go where they should
prev_dir = os.getcwd()
try:
for domain in self.domains:
for kind in ALL_FOUR:
os.chdir(os.path.dirname(self.config_files[domain][kind]))
assert filesystem.realpath(filesystem.readlink(self.config_files[domain][kind])) == \
filesystem.realpath(archive_paths[domain][kind])
finally:
os.chdir(prev_dir)
class DeleteTest(storage_test.BaseRenewableCertTest):
"""Tests for certbot._internal.cert_manager.delete
"""

View File

@@ -552,55 +552,6 @@ class ParseTest(unittest.TestCase):
])
assert_value_and_source(namespace, 'server', COMMAND_LINE_VALUE, ArgumentSource.COMMAND_LINE)
def test_abbreviated_arguments(self):
# Argparse's "allow_abbrev" option (which is True by default) allows
# for unambiguous partial arguments (e.g. "--preferred-chal dns" will be
# interepreted the same as "--preferred-challenges dns")
namespace = self.parse('--preferred-chal dns --no-dir')
assert_set_by_user_with_value(namespace, 'pref_challs', ['dns-01'])
assert_set_by_user_with_value(namespace, 'directory_hooks', False)
with tempfile.NamedTemporaryFile() as tmp_config:
tmp_config.close() # close now because of compatibility issues on Windows
with open(tmp_config.name, 'w') as file_h:
file_h.write('preferred-chal = dns')
namespace = self.parse([
'certonly',
'--config', tmp_config.name,
])
assert_set_by_user_with_value(namespace, 'pref_challs', ['dns-01'])
@mock.patch('certbot._internal.hooks.validate_hooks')
def test_argument_with_equals(self, unsused_mock_validate_hooks):
namespace = self.parse('-d=example.com')
assert_set_by_user_with_value(namespace, 'domains', ['example.com'])
# make sure it doesn't choke on equals signs being present in the argument value
plugins = disco.PluginsRegistry.find_all()
namespace = cli.prepare_and_parse_args(plugins, ['run', '--pre-hook="foo=bar"'])
assert_set_by_user_with_value(namespace, 'pre_hook', '"foo=bar"')
def test_adjacent_short_args(self):
namespace = self.parse('-tv')
assert_set_by_user_with_value(namespace, 'text_mode', True)
assert_set_by_user_with_value(namespace, 'verbose_count', 1)
namespace = self.parse('-tvvv')
assert_set_by_user_with_value(namespace, 'text_mode', True)
assert_set_by_user_with_value(namespace, 'verbose_count', 3)
namespace = self.parse('-tvm foo@example.com')
assert_set_by_user_with_value(namespace, 'text_mode', True)
assert_set_by_user_with_value(namespace, 'verbose_count', 1)
assert_set_by_user_with_value(namespace, 'email', 'foo@example.com')
def test_arg_with_contained_spaces(self):
# This can happen if a user specifies an arg like "-d foo.com" enclosed
# in double quotes, or as its own line in a docker-compose.yml file (as
# in #9811)
namespace = self.parse(['certonly', '-d foo.com'])
assert_set_by_user_with_value(namespace, 'domains', ['foo.com'])
if __name__ == '__main__':
sys.exit(pytest.main(sys.argv[1:] + [__file__])) # pragma: no cover

View File

@@ -48,6 +48,7 @@ class NamespaceConfigTest(test_util.ConfigTestCase):
def test_dynamic_dirs(self, mock_constants):
mock_constants.ACCOUNTS_DIR = 'acc'
mock_constants.BACKUP_DIR = 'backups'
mock_constants.CSR_DIR = 'csr'
mock_constants.IN_PROGRESS_DIR = '../p'
mock_constants.KEY_DIR = 'keys'
@@ -59,6 +60,12 @@ class NamespaceConfigTest(test_util.ConfigTestCase):
os.path.normpath(os.path.join(self.config.config_dir, ref_path))
assert os.path.normpath(self.config.backup_dir) == \
os.path.normpath(os.path.join(self.config.work_dir, 'backups'))
with warnings.catch_warnings():
warnings.simplefilter("ignore", DeprecationWarning)
assert os.path.normpath(self.config.csr_dir) == \
os.path.normpath(os.path.join(self.config.config_dir, 'csr'))
assert os.path.normpath(self.config.key_dir) == \
os.path.normpath(os.path.join(self.config.config_dir, 'keys'))
assert os.path.normpath(self.config.in_progress_dir) == \
os.path.normpath(os.path.join(self.config.work_dir, '../p'))
assert os.path.normpath(self.config.temp_checkpoint_dir) == \
@@ -93,6 +100,10 @@ class NamespaceConfigTest(test_util.ConfigTestCase):
os.path.join(os.getcwd(), logs_base)
assert os.path.isabs(config.accounts_dir)
assert os.path.isabs(config.backup_dir)
with warnings.catch_warnings():
warnings.simplefilter("ignore", DeprecationWarning)
assert os.path.isabs(config.csr_dir)
assert os.path.isabs(config.key_dir)
assert os.path.isabs(config.in_progress_dir)
assert os.path.isabs(config.temp_checkpoint_dir)
@@ -154,22 +165,17 @@ class NamespaceConfigTest(test_util.ConfigTestCase):
def test_set_by_user_exception(self):
from certbot.configuration import NamespaceConfig
# a newly created NamespaceConfig has no argument sources dict, so an
# exception is raised
config = NamespaceConfig(self.config.namespace)
with pytest.raises(RuntimeError):
config.set_by_user('whatever')
# now set an argument sources dict
config.set_argument_sources({})
assert not config.set_by_user('whatever')
def test_set_by_user_mutables(self):
assert not self.config.set_by_user('domains')
self.config.domains.append('example.org')
assert self.config.set_by_user('domains')
if __name__ == '__main__':
sys.exit(pytest.main(sys.argv[1:] + [__file__])) # pragma: no cover

View File

@@ -168,7 +168,7 @@ class MakeKeyTest(unittest.TestCase):
from certbot.crypto_util import make_key
# Do not test larger keys as it takes too long.
OpenSSL.crypto.load_privatekey(OpenSSL.crypto.FILETYPE_PEM, make_key(2048))
OpenSSL.crypto.load_privatekey(OpenSSL.crypto.FILETYPE_PEM, make_key(1024))
def test_ec(self): # pylint: disable=no-self-use
# ECDSA Key Type Tests
@@ -185,8 +185,8 @@ class MakeKeyTest(unittest.TestCase):
from certbot.crypto_util import make_key
# Try a bad key size for RSA and ECDSA
with pytest.raises(errors.Error, match='Unsupported RSA key length: 1024'):
make_key(bits=1024, key_type='rsa')
with pytest.raises(errors.Error, match='Unsupported RSA key length: 512'):
make_key(bits=512, key_type='rsa')
def test_bad_elliptic_curve_name(self):
from certbot.crypto_util import make_key
@@ -200,7 +200,7 @@ class MakeKeyTest(unittest.TestCase):
with pytest.raises(errors.Error,
match=re.escape('Invalid key_type specified: unf. Use [rsa|ecdsa]')):
OpenSSL.crypto.load_privatekey(
OpenSSL.crypto.FILETYPE_PEM, make_key(2048, key_type='unf'))
OpenSSL.crypto.FILETYPE_PEM, make_key(1024, key_type='unf'))
class VerifyCertSetup(unittest.TestCase):

View File

@@ -194,25 +194,6 @@ class FileOutputDisplayTest(unittest.TestCase):
self.displayer._scrub_checklist_input(list_, TAGS))
assert set_tags == exp[i]
def test_scrub_checklist_maintain_indices_order(self):
# pylint: disable=protected-access
source_tags = ["T1", "T2", "T3", "T4", "T5", "T6", "T7", "T8", "T9"]
indices = [
["4", "9"],
["9", "4"],
["4", "9", "4"],
["9", "4", "9"],
]
exp = [
["T4", "T9"],
["T9", "T4"],
["T4", "T9"],
["T9", "T4"],
]
for i, list_ in enumerate(indices):
tags = self.displayer._scrub_checklist_input(list_, source_tags)
assert tags == exp[i]
@mock.patch("certbot._internal.display.util.input_with_timeout")
def test_directory_select(self, mock_input):
args = ["msg", "/var/www/html", "--flag", True]

View File

@@ -563,7 +563,7 @@ class ReconfigureTest(test_util.TempDirTestCase):
# Options used in the renewal process
[renewalparams]
account = ee43634db0aa4e6804f152be39990e6a
server = https://acme-v02.api.letsencrypt.org/directory
server = https://acme-staging-v02.api.letsencrypt.org/directory
authenticator = nginx
installer = nginx
key_type = rsa
@@ -621,72 +621,6 @@ class ReconfigureTest(test_util.TempDirTestCase):
new_config = self._call('--cert-name example.com --apache'.split())
assert new_config['renewalparams']['authenticator'] == 'apache'
def test_only_intended_changes(self):
""" Check that we don't accidentally modify anything that we didn't mean to """
named_mock = mock.Mock()
named_mock.name = 'apache'
self.mocks['pick_installer'].return_value = named_mock
self.mocks['pick_auth'].return_value = named_mock
self.mocks['find_init'].return_value = named_mock
new_config = self._call('--cert-name example.com --apache'.split())
# Undo the changes we made in calling and in testing
new_config['renewalparams']['authenticator'] = 'nginx'
new_config['renewalparams']['installer'] = 'nginx'
del new_config['renewalparams']['config_dir']
new_config['version'] = self.original_config['version']
assert new_config == self.original_config
@mock.patch('certbot._internal.hooks.validate_hooks')
def test_staging_used(self, unused_validate_hooks):
""" Check that we use the staging server for the dry run """
assert self.original_config['renewalparams']['server'] == \
'https://acme-v02.api.letsencrypt.org/directory'
self._call('--cert-name example.com --pre-hook'.split() + ['echo pre'])
assert 'staging' in self.mocks['_init_le_client'].call_args.args[0].server
assert 'staging' in self.mocks['_get_and_save_cert'].call_args.args[1].server
def test_new_account_or_server_errors(self):
""" Check that we error when attempting to change the account id or server,
but not when it's the same
"""
orig_account_id = self.original_config['renewalparams']['account']
orig_server = self.original_config['renewalparams']['server']
# new account
try:
self._call(f'--cert-name example.com --account newaccountid'.split())
except errors.ConfigurationError as err:
assert "Using reconfigure to change the ACME account" in str(err)
# check that config isn't modified
with open(self.renewal_file, 'r') as f:
new_config = configobj.ConfigObj(f, encoding='utf-8', default_encoding='utf-8')
assert new_config['renewalparams']['account'] == orig_account_id
# same account
new_config = self._call(f'--cert-name example.com --account {orig_account_id}'.split())
assert new_config['renewalparams']['account'] == orig_account_id
# new server
try:
self._call(f'--cert-name example.com --server x.com'.split())
except errors.ConfigurationError as err:
assert "Using reconfigure to change the ACME account" in str(err)
# check that config isn't modified
with open(self.renewal_file, 'r') as f:
new_config = configobj.ConfigObj(f, encoding='utf-8', default_encoding='utf-8')
assert new_config['renewalparams']['server'] == orig_server
# same server
new_config = self._call(f'--cert-name example.com --server {orig_server}'.split())
assert new_config['renewalparams']['server'] == orig_server
@mock.patch('certbot._internal.hooks.validate_hooks')
def test_update_hooks(self, unused_validate_hooks):
assert 'pre_hook' not in self.original_config
@@ -1215,6 +1149,11 @@ class MainTest(test_util.ConfigTestCase):
client.rollback.assert_called_once_with(
mock.ANY, 123, mock.ANY, mock.ANY)
@mock.patch('certbot._internal.cert_manager.update_live_symlinks')
def test_update_symlinks(self, mock_cert_manager):
self._call_no_clientmock(['update_symlinks'])
assert 1 == mock_cert_manager.call_count
@mock.patch('certbot._internal.cert_manager.certificates')
def test_certificates(self, mock_cert_manager):
self._call_no_clientmock(['certificates'])

View File

@@ -253,18 +253,6 @@ class RestoreRequiredConfigElementsTest(test_util.ConfigTestCase):
self._call(self.config, {'server': constants.V1_URI})
assert self.config.server == constants.CLI_DEFAULTS['server']
def test_related_values(self):
# certbot.configuration.NamespaceConfig.set_by_user considers some values as related to each
# other and considers both set by the user if either is. This test ensures all renewal
# parameters are restored regardless of their restoration order or relation between values.
# See https://github.com/certbot/certbot/issues/9805 for more info.
renewalparams = {
'server': 'https://example.org',
'account': 'somehash',
}
self._call(self.config, renewalparams)
self.assertEqual(self.config.account, renewalparams['account'])
class DescribeResultsTest(unittest.TestCase):
"""Tests for certbot._internal.renewal._renew_describe_results."""

View File

@@ -838,6 +838,21 @@ class RenewableCertTests(BaseRenewableCertTest):
assert stat.S_IMODE(os.lstat(temp).st_mode) == \
stat.S_IMODE(os.lstat(temp2).st_mode)
def test_update_symlinks(self):
from certbot._internal import storage
archive_dir_path = os.path.join(self.config.config_dir, "archive", "example.org")
for kind in ALL_FOUR:
live_path = self.config_file[kind]
basename = kind + "1.pem"
archive_path = os.path.join(archive_dir_path, basename)
open(archive_path, 'a').close()
os.symlink(os.path.join(self.config.config_dir, basename), live_path)
with pytest.raises(errors.CertStorageError):
storage.RenewableCert(self.config_file.filename,
self.config)
storage.RenewableCert(self.config_file.filename, self.config,
update_symlinks=True)
def test_truncate(self):
# It should not do anything when there's less than 5 cert history
for kind in ALL_FOUR:

View File

@@ -8,6 +8,7 @@ from typing import Dict
from typing import List
from typing import Optional
from urllib import parse
import warnings
from certbot import errors
from certbot import util
@@ -42,7 +43,9 @@ class NamespaceConfig:
paths defined in :py:mod:`certbot._internal.constants`:
- `accounts_dir`
- `csr_dir`
- `in_progress_dir`
- `key_dir`
- `temp_checkpoint_dir`
And the following paths are dynamically resolved using
@@ -63,8 +66,7 @@ class NamespaceConfig:
self.namespace: argparse.Namespace
# Avoid recursion loop because of the delegation defined in __setattr__
object.__setattr__(self, 'namespace', namespace)
object.__setattr__(self, '_argument_sources', None)
object.__setattr__(self, '_previously_accessed_mutables', {})
object.__setattr__(self, 'argument_sources', None)
self.namespace.config_dir = os.path.abspath(self.namespace.config_dir)
self.namespace.work_dir = os.path.abspath(self.namespace.work_dir)
@@ -88,7 +90,7 @@ class NamespaceConfig:
"""
# Avoid recursion loop because of the delegation defined in __setattr__
object.__setattr__(self, '_argument_sources', argument_sources)
object.__setattr__(self, 'argument_sources', argument_sources)
def set_by_user(self, var: str) -> bool:
@@ -143,48 +145,15 @@ class NamespaceConfig:
"""
If an argument_sources dict was set, overwrites an argument's source to
be ArgumentSource.RUNTIME. Used when certbot sets an argument's values
at runtime. This also clears the modified value from
_previously_accessed_mutables since it is no longer needed.
at runtime.
"""
if self._argument_sources is not None:
self._argument_sources[name] = ArgumentSource.RUNTIME
if name in self._previously_accessed_mutables:
del self._previously_accessed_mutables[name]
@property
def argument_sources(self) -> Optional[Dict[str, ArgumentSource]]:
"""Returns _argument_sources after handling any changes to accessed mutable values."""
# We keep values in _previously_accessed_mutables until we've detected a modification to try
# to provide up-to-date information when argument_sources is accessed. Once a mutable object
# has been accessed, it can be modified at any time if a reference to it was kept somewhere
# else.
# We copy _previously_accessed_mutables because _mark_runtime_override modifies it.
for name, prev_value in self._previously_accessed_mutables.copy().items():
current_value = getattr(self.namespace, name)
if current_value != prev_value:
self._mark_runtime_override(name)
return self._argument_sources
if self.argument_sources is not None:
self.argument_sources[name] = ArgumentSource.RUNTIME
# Delegate any attribute not explicitly defined to the underlying namespace object.
#
# If any mutable namespace attributes are explicitly defined in the future, you'll probably want
# to take an approach like the one used in __getattr__ and the argument_sources property.
def __getattr__(self, name: str) -> Any:
arg_sources = self.argument_sources
value = getattr(self.namespace, name)
if arg_sources is not None:
# If the requested attribute was already modified at runtime, we don't need to track any
# future changes.
if name not in arg_sources or arg_sources[name] != ArgumentSource.RUNTIME:
# If name is already in _previously_accessed_mutables, we don't need to make a copy
# of it again. If its value was changed, this would have been caught while preparing
# the return value of the property self.argument_sources accessed earlier in this
# function.
if name not in self._previously_accessed_mutables and not _is_immutable(value):
self._previously_accessed_mutables[name] = copy.deepcopy(value)
return value
return getattr(self.namespace, name)
def __setattr__(self, name: str, value: Any) -> None:
self._mark_runtime_override(name)
@@ -282,11 +251,25 @@ class NamespaceConfig:
"""Configuration backups directory."""
return os.path.join(self.namespace.work_dir, constants.BACKUP_DIR)
@property
def csr_dir(self) -> str:
"""Directory where new Certificate Signing Requests (CSRs) are saved."""
warnings.warn("NamespaceConfig.csr_dir is deprecated and will be removed in an upcoming "
"release of Certbot", DeprecationWarning)
return os.path.join(self.namespace.config_dir, constants.CSR_DIR)
@property
def in_progress_dir(self) -> str:
"""Directory used before a permanent checkpoint is finalized."""
return os.path.join(self.namespace.work_dir, constants.IN_PROGRESS_DIR)
@property
def key_dir(self) -> str:
"""Keys storage."""
warnings.warn("NamespaceConfig.key_dir is deprecated and will be removed in an upcoming "
"release of Certbot", DeprecationWarning)
return os.path.join(self.namespace.config_dir, constants.KEY_DIR)
@property
def temp_checkpoint_dir(self) -> str:
"""Temporary checkpoint directory."""
@@ -442,10 +425,9 @@ class NamespaceConfig:
# Work around https://bugs.python.org/issue1515 for py26 tests :( :(
new_ns = copy.deepcopy(self.namespace)
new_config = type(self)(new_ns)
# Avoid recursion loop because of the delegation defined in __setattr__
object.__setattr__(new_config, '_argument_sources', copy.deepcopy(self.argument_sources))
object.__setattr__(new_config, '_previously_accessed_mutables',
copy.deepcopy(self._previously_accessed_mutables))
if self.set_argument_sources is not None:
new_sources = copy.deepcopy(self.argument_sources)
new_config.set_argument_sources(new_sources)
return new_config
@@ -468,15 +450,3 @@ def _check_config_sanity(config: NamespaceConfig) -> None:
for domain in config.namespace.domains:
# This may be redundant, but let's be paranoid
util.enforce_domain_sanity(domain)
def _is_immutable(value: Any) -> bool:
"""Is value of an immutable type?"""
if isinstance(value, tuple):
# tuples are only immutable if all contained values are immutable.
return all(_is_immutable(subvalue) for subvalue in value)
for immutable_type in (int, float, complex, str, bytes, bool, frozenset,):
if isinstance(value, immutable_type):
return True
# The last case we consider here is None which is also immutable.
return value is None

View File

@@ -208,11 +208,11 @@ def import_csr_file(csrfile: str, data: bytes) -> Tuple[int, util.CSR, List[str]
return PEM, util.CSR(file=csrfile, data=data_pem, form="pem"), domains
def make_key(bits: int = 2048, key_type: str = "rsa",
def make_key(bits: int = 1024, key_type: str = "rsa",
elliptic_curve: Optional[str] = None) -> bytes:
"""Generate PEM encoded RSA|EC key.
:param int bits: Number of bits if key_type=rsa. At least 2048 for RSA.
:param int bits: Number of bits if key_type=rsa. At least 1024 for RSA.
:param str key_type: The type of key to generate, but be rsa or ecdsa
:param str elliptic_curve: The elliptic curve to use.
@@ -221,7 +221,7 @@ def make_key(bits: int = 2048, key_type: str = "rsa",
:rtype: str
"""
if key_type == 'rsa':
if bits < 2048:
if bits < 1024:
raise errors.Error("Unsupported RSA key length: {}".format(bits))
key = crypto.PKey()

View File

@@ -465,7 +465,7 @@ def dir_setup(test_dir: str, pkg: str) -> Tuple[str, str, str]: # pragma: no co
filesystem.chmod(config_dir, constants.CONFIG_DIRS_MODE)
filesystem.chmod(work_dir, constants.CONFIG_DIRS_MODE)
test_dir_ref = importlib_resources.files(pkg).joinpath("testdata").joinpath(test_dir)
test_dir_ref = importlib_resources.files(pkg).joinpath("testdata", test_dir)
with importlib_resources.as_file(test_dir_ref) as path:
shutil.copytree(
path, os.path.join(temp_dir, test_dir), symlinks=True)

View File

@@ -198,10 +198,6 @@ class LexiconDNSAuthenticator(dns_common.DNSAuthenticator):
dict_config = {
'domain': domain,
# We bypass Lexicon subdomain resolution by setting the 'delegated' field in the config
# to the value of the 'domain' field itself. Here we consider that the domain passed to
# _build_lexicon_config() is already the exact subdomain of the actual DNS zone to use.
'delegated': domain,
'provider_name': self._provider_name,
'ttl': self._ttl,
self._provider_name: {item[2]: self._credentials.conf(item[0])

View File

@@ -1,7 +1,6 @@
"""Test utilities."""
import atexit
from contextlib import ExitStack
import copy
from importlib import reload as reload_module
import io
import logging
@@ -404,8 +403,7 @@ class ConfigTestCase(TempDirTestCase):
def setUp(self) -> None:
super().setUp()
self.config = configuration.NamespaceConfig(
# We make a copy here so any mutable values from CLI_DEFAULTS do not get modified.
mock.MagicMock(**copy.deepcopy(constants.CLI_DEFAULTS)),
mock.MagicMock(**constants.CLI_DEFAULTS),
)
self.config.set_argument_sources({})
self.config.namespace.verb = "certonly"

View File

@@ -122,7 +122,7 @@ options:
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/2.11.0 (certbot;
"". (default: CertbotACMEClient/2.7.0 (certbot;
OS_NAME OS_VERSION) Authenticator/XXX Installer/YYY
(SUBCOMMAND; flags: FLAGS) Py/major.minor.patchlevel).
The flags encoded in the user agent are: --duplicate,
@@ -734,7 +734,7 @@ standalone:
webroot:
Saves the necessary validation files to a .well-known/acme-challenge/
directory within the nominated webroot path. A separate HTTP server must
directory within the nominated webroot path. A seperate HTTP server must
be running and serving files from the webroot path. HTTP challenge only
(wildcards not supported).

View File

@@ -69,7 +69,7 @@ master_doc = 'index'
# General information about the project.
project = u'Certbot'
# this is now overridden by the footer.html template
copyright = u'2014-2018 - The Certbot software and documentation are licensed under the Apache 2.0 license as described at https://eff.org/cb-license.'
#copyright = u'2014-2018 - The Certbot software and documentation are licensed under the Apache 2.0 license as described at https://eff.org/cb-license.'
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the

View File

@@ -17,11 +17,8 @@ its dependencies, Certbot needs to be run on a UNIX-like OS so if you're using
Windows, you'll need to set up a (virtual) machine running an OS such as Linux
and continue with these instructions on that UNIX-like OS.
If you're using macOS, it is recommended to first check out the `macOS
suggestions`_ section before continuing with the installation instructions
below.
.. _local copy:
.. _prerequisites:
Running a local copy of the client
----------------------------------
@@ -51,10 +48,9 @@ Install and configure the OS system dependencies required to run Certbot.
sudo dnf install python3 augeas-libs
# For macOS installations with Homebrew already installed and configured
# NB: If you also run `brew install python` you don't need the ~/lib
# directory created below, however, without this directory and symlinks
# to augeas, Certbot's Apache plugin won't work if you use Python
# installed from other sources such as pyenv or the version provided by
# Apple.
# directory created below, however, Certbot's Apache plugin won't work
# if you use Python installed from other sources such as pyenv or the
# version provided by Apple.
brew install augeas
mkdir ~/lib
ln -s $(brew --prefix)/lib/libaugeas* ~/lib
@@ -138,7 +134,7 @@ For debugging, we recommend putting
Once you are done with your code changes, and the tests in ``foo_test.py``
pass, run all of the unit tests for Certbot and check for coverage with ``tox
-e cover``. You should then check for code style with ``tox run -e lint`` (all
-e cover``. You should then check for code style with ``tox -e lint`` (all
files) or ``pylint --rcfile=.pylintrc path/to/file.py`` (single file at a
time).
@@ -158,7 +154,7 @@ Running automated integration tests
Generally it is sufficient to open a pull request and let Github and Azure Pipelines run
integration tests for you. However, you may want to run them locally before submitting
your pull request. You need Docker installed and working.
your pull request. You need Docker and docker-compose installed and working.
The tox environment `integration` will setup `Pebble`_, the Let's Encrypt ACME CA server
for integration testing, then launch the Certbot integration tests.
@@ -167,7 +163,7 @@ With a user allowed to access your local Docker daemon, run:
.. code-block:: shell
tox run -e integration
tox -e integration
Tests will be run using pytest. A test report and a code coverage report will be
displayed at the end of the integration tests execution.
@@ -332,8 +328,8 @@ Writing your own plugin
for one example of that.
Certbot client supports dynamic discovery of plugins through the
`importlib.metadata entry points`_ using the `certbot.plugins` group.
This way you can, for example, create a custom implementation of
`setuptools entry points`_ using the `certbot.plugins` group. This
way you can, for example, create a custom implementation of
`~certbot.interfaces.Authenticator` or the
`~certbot.interfaces.Installer` without having to merge it
with the core upstream source code. An example is provided in
@@ -356,8 +352,8 @@ users install it system-wide with `pip install`. Note that this will
only work for users who have Certbot installed from OS packages or via
pip.
.. _`importlib.metadata entry points`:
https://importlib-metadata.readthedocs.io/en/latest/using.html#entry-points
.. _`setuptools entry points`:
https://setuptools.readthedocs.io/en/latest/pkg_resources.html#entry-points
Writing your own plugin snap
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -379,8 +375,8 @@ Certbot plugin snaps expose their Python modules to the Certbot snap via a
`snap content interface`_ where ``certbot-1`` is the value for the ``content``
attribute. The Certbot snap only uses this to find the names of connected
plugin snaps and it expects to find the Python modules to be loaded under
``lib/python3.12/site-packages/`` in the plugin snap. This location is the
default when using the ``core24`` `base snap`_ and the `python snapcraft
``lib/python3.8/site-packages/`` in the plugin snap. This location is the
default when using the ``core20`` `base snap`_ and the `python snapcraft
plugin`_.
The Certbot snap also provides a separate content interface which
@@ -489,7 +485,7 @@ annotations; we can find bugs in Certbot even without a fully annotated codebase
Zulip wrote a `great guide`_ to using mypy. Its useful, but you dont have to read the whole thing
to start contributing to Certbot.
To run mypy on Certbot, use ``tox run -e mypy`` on a machine that has Python 3 installed.
To run mypy on Certbot, use ``tox -e mypy`` on a machine that has Python 3 installed.
Also note that OpenSSL, which we rely on, has type definitions for crypto but not SSL. We use both.
Those imports should look like this:
@@ -513,12 +509,12 @@ Steps:
something we have the time and interest to review.
1. Write your code! When doing this, you should add :ref:`mypy type annotations
<type annotations>` for any functions you add or modify. You can check that
you've done this correctly by running ``tox run -e mypy`` on a machine that has
you've done this correctly by running ``tox -e mypy`` on a machine that has
Python 3 installed.
2. Make sure your environment is set up properly and that you're in your
virtualenv. You can do this by following the instructions in the
:ref:`Getting Started <getting_started>` section.
3. Run ``tox run -e lint`` to check for pylint errors. Fix any errors.
3. Run ``tox -e lint`` to check for pylint errors. Fix any errors.
4. Run ``tox --skip-missing-interpreters`` to run all the tests we recommend
developers run locally. The ``--skip-missing-interpreters`` argument ignores
missing versions of Python needed for running the tests. Fix any errors.
@@ -651,28 +647,3 @@ If a dependency is already packaged in these distros and is acceptable for use i
the oldest packaged version of that dependency should be chosen and set as the minimum
version in ``setup.py``.
macOS suggestions
=================
If you're developing on macOS, before :ref:`setting up your Certbot development
environment <local copy>`, it is recommended you perform the following steps.
None of this is required, but it is the approach used by all/most of the
current Certbot developers on macOS as of writing this:
0. Install `Homebrew <https://brew.sh/>`_. It is the most popular package
manager on macOS by a wide margin and works well enough.
1. Install `pyenv <https://github.com/pyenv/pyenv>`_, ideally through Homebrew
by running ``brew install pyenv``. Using Homebrew's Python for Certbot
development is annoying because it regularly updates and every time it does
it breaks your virtual environments. Using Python from ``pyenv`` avoids this
problem and gives you easy access to all versions of Python.
2. If you're using ``pyenv``, make sure you've set up your shell for it by
following instructions like
https://github.com/pyenv/pyenv?tab=readme-ov-file#set-up-your-shell-environment-for-pyenv.
3. Configure ``git`` to ignore the ``.DS_Store`` files that are created by
macOS's file manager Finder by running something like:
.. code-block:: shell
mkdir -p ~/.config/git
echo '.DS_Store' >> ~/.config/git/ignore

View File

@@ -325,9 +325,6 @@ dns-multi_ Y N DNS authentication of 100+ providers using go-acme/
dns-dnsmanager_ Y N DNS Authentication for dnsmanager.io
standalone-nfq_ Y N HTTP Authentication that works with any webserver (Linux only)
dns-solidserver_ Y N DNS Authentication using SOLIDserver (EfficientIP)
dns-stackit_ Y N DNS Authentication using STACKIT DNS
dns-ionos_ Y N DNS Authentication using IONOS Cloud DNS
dns-mijn-host_ Y N DNS Authentication using mijn.host DNS
================== ==== ==== ===============================================================
.. _haproxy: https://github.com/greenhost/certbot-haproxy
@@ -354,9 +351,6 @@ dns-mijn-host_ Y N DNS Authentication using mijn.host DNS
.. _dns-dnsmanager: https://github.com/stayallive/certbot-dns-dnsmanager
.. _standalone-nfq: https://github.com/alexzorin/certbot-standalone-nfq
.. _dns-solidserver: https://gitlab.com/charlyhong/certbot-dns-solidserver
.. _dns-stackit: https://github.com/stackitcloud/certbot-dns-stackit
.. _dns-ionos: https://github.com/ionos-cloud/certbot-dns-ionos-cloud
.. _dns-mijn-host: https://github.com/mijnhost/certbot-dns-mijn-host
If you're interested, you can also :ref:`write your own plugin <dev-plugin>`.

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