Compare commits

..

5 Commits

529 changed files with 12613 additions and 14858 deletions

View File

@@ -1,14 +0,0 @@
# Advanced pipeline for running our full test suite on demand.
trigger:
# When changing these triggers, please ensure the documentation under
# "Running tests in CI" is still correct.
- test-*
pr: none
variables:
# We don't publish our Docker images in this pipeline, but when building them
# for testing, let's use the nightly tag.
dockerTag: nightly
stages:
- template: templates/stages/test-and-package-stage.yml

View File

@@ -0,0 +1,23 @@
# Advanced pipeline for isolated checks and release purpose
trigger:
# When changing these triggers, please ensure the documentation under
# "Running tests in CI" is still correct.
- azure-test-*
- test-*
- '*.x'
pr:
- test-*
# This pipeline is also nightly run on master
schedules:
- cron: "0 4 * * *"
displayName: Nightly build
branches:
include:
- master
always: true
jobs:
# Any addition here should be reflected in the release pipeline.
# It is advised to declare all jobs here as templates to improve maintainability.
- template: templates/tests-suite.yml
- template: templates/installer-tests.yml

View File

@@ -1,8 +1,12 @@
trigger: none
trigger:
# apache-parser-v2 is a temporary branch for doing work related to
# rewriting the parser in the Apache plugin.
- apache-parser-v2
- master
pr:
- apache-parser-v2
- master
- '*.x'
jobs:
- template: templates/jobs/standard-tests-jobs.yml
- template: templates/tests-suite.yml

View File

@@ -1,18 +0,0 @@
# Nightly pipeline running each day for master.
trigger: none
pr: none
schedules:
- cron: "30 4 * * *"
displayName: Nightly build
branches:
include:
- master
always: true
variables:
dockerTag: nightly
stages:
- template: templates/stages/test-and-package-stage.yml
- template: templates/stages/deploy-stage.yml
- template: templates/stages/notify-failure-stage.yml

View File

@@ -1,18 +1,13 @@
# Release pipeline to run our full test suite, build artifacts, and deploy them
# for GitHub release tags.
# Release pipeline to build and deploy Certbot for Windows for GitHub release tags
trigger:
tags:
include:
- v*
pr: none
variables:
dockerTag: ${{variables['Build.SourceBranchName']}}
stages:
- template: templates/stages/test-and-package-stage.yml
- template: templates/stages/changelog-stage.yml
- template: templates/stages/deploy-stage.yml
parameters:
snapReleaseChannel: beta
- template: templates/stages/notify-failure-stage.yml
jobs:
# Any addition here should be reflected in the advanced pipeline.
# It is advised to declare all jobs here as templates to improve maintainability.
- template: templates/tests-suite.yml
- template: templates/installer-tests.yml
- template: templates/changelog.yml

View File

@@ -0,0 +1,14 @@
jobs:
- job: changelog
pool:
vmImage: vs2017-win2016
steps:
- bash: |
CERTBOT_VERSION="$(cd certbot && python -c "import certbot; print(certbot.__version__)" && cd ~-)"
"${BUILD_REPOSITORY_LOCALPATH}\tools\extract_changelog.py" "${CERTBOT_VERSION}" >> "${BUILD_ARTIFACTSTAGINGDIRECTORY}/release_notes.md"
displayName: Prepare changelog
- task: PublishPipelineArtifact@1
inputs:
path: $(Build.ArtifactStagingDirectory)
artifact: changelog
displayName: Publish changelog

View File

@@ -0,0 +1,56 @@
jobs:
- job: installer_build
pool:
vmImage: vs2017-win2016
steps:
- task: UsePythonVersion@0
inputs:
versionSpec: 3.7
architecture: x86
addToPath: true
- script: python windows-installer/construct.py
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)
artifact: windows-installer
displayName: Publish Windows installer
- job: installer_run
dependsOn: installer_build
strategy:
matrix:
win2019:
imageName: windows-2019
win2016:
imageName: vs2017-win2016
win2012r2:
imageName: vs2015-win2012r2
pool:
vmImage: $(imageName)
steps:
- powershell: Invoke-WebRequest https://www.python.org/ftp/python/3.8.1/python-3.8.1-amd64-webinstall.exe -OutFile C:\py3-setup.exe
displayName: Get Python
- script: C:\py3-setup.exe /quiet PrependPath=1 InstallAllUsers=1 Include_launcher=1 InstallLauncherAllUsers=1 Include_test=0 Include_doc=0 Include_dev=1 Include_debug=0 Include_tcltk=0 TargetDir=C:\py3
displayName: Install Python
- task: DownloadPipelineArtifact@2
inputs:
artifact: windows-installer
path: $(Build.SourcesDirectory)/bin
displayName: Retrieve Windows installer
- script: |
py -3 -m venv venv
venv\Scripts\python tools\pip_install.py -e certbot-ci
displayName: Prepare Certbot-CI
- script: |
set PATH=%ProgramFiles(x86)%\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-win32.exe
displayName: Run windows installer integration tests
- script: |
set PATH=%ProgramFiles(x86)%\Certbot\bin;%PATH%
venv\Scripts\python -m pytest certbot-ci\certbot_integration_tests\certbot_tests -n 4
displayName: Run certbot integration tests

View File

@@ -1,94 +0,0 @@
jobs:
- job: extended_test
variables:
- name: IMAGE_NAME
value: ubuntu-18.04
- name: PYTHON_VERSION
value: 3.9
- group: certbot-common
strategy:
matrix:
linux-py36:
PYTHON_VERSION: 3.6
TOXENV: py36
linux-py37:
PYTHON_VERSION: 3.7
TOXENV: py37
linux-py38:
PYTHON_VERSION: 3.8
TOXENV: py38
linux-py37-nopin:
PYTHON_VERSION: 3.7
TOXENV: py37
CERTBOT_NO_PIN: 1
linux-external-mock:
TOXENV: external-mock
linux-boulder-v1-integration-certbot-oldest:
PYTHON_VERSION: 3.6
TOXENV: integration-certbot-oldest
ACME_SERVER: boulder-v1
linux-boulder-v2-integration-certbot-oldest:
PYTHON_VERSION: 3.6
TOXENV: integration-certbot-oldest
ACME_SERVER: boulder-v2
linux-boulder-v1-integration-nginx-oldest:
PYTHON_VERSION: 3.6
TOXENV: integration-nginx-oldest
ACME_SERVER: boulder-v1
linux-boulder-v2-integration-nginx-oldest:
PYTHON_VERSION: 3.6
TOXENV: integration-nginx-oldest
ACME_SERVER: boulder-v2
linux-boulder-v1-py36-integration:
PYTHON_VERSION: 3.6
TOXENV: integration
ACME_SERVER: boulder-v1
linux-boulder-v2-py36-integration:
PYTHON_VERSION: 3.6
TOXENV: integration
ACME_SERVER: boulder-v2
linux-boulder-v1-py37-integration:
PYTHON_VERSION: 3.7
TOXENV: integration
ACME_SERVER: boulder-v1
linux-boulder-v2-py37-integration:
PYTHON_VERSION: 3.7
TOXENV: integration
ACME_SERVER: boulder-v2
linux-boulder-v1-py38-integration:
PYTHON_VERSION: 3.8
TOXENV: integration
ACME_SERVER: boulder-v1
linux-boulder-v2-py38-integration:
PYTHON_VERSION: 3.8
TOXENV: integration
ACME_SERVER: boulder-v2
linux-boulder-v1-py39-integration:
PYTHON_VERSION: 3.9
TOXENV: integration
ACME_SERVER: boulder-v1
linux-boulder-v2-py39-integration:
PYTHON_VERSION: 3.9
TOXENV: integration
ACME_SERVER: boulder-v2
nginx-compat:
TOXENV: nginx_compat
linux-integration-rfc2136:
IMAGE_NAME: ubuntu-18.04
PYTHON_VERSION: 3.8
TOXENV: integration-dns-rfc2136
docker-dev:
TOXENV: docker_dev
macos-farmtest-apache2:
# We run one of these test farm tests on macOS to help ensure the
# tests continue to work on the platform.
IMAGE_NAME: macOS-10.15
PYTHON_VERSION: 3.8
TOXENV: test-farm-apache2
farmtest-sdists:
PYTHON_VERSION: 3.7
TOXENV: test-farm-sdists
pool:
vmImage: $(IMAGE_NAME)
steps:
- template: ../steps/tox-steps.yml

View File

@@ -1,230 +0,0 @@
jobs:
- job: docker_build
pool:
vmImage: ubuntu-18.04
strategy:
matrix:
amd64:
DOCKER_ARCH: amd64
# Do not run the heavy non-amd64 builds for test branches
${{ if not(startsWith(variables['Build.SourceBranchName'], 'test-')) }}:
arm32v6:
DOCKER_ARCH: arm32v6
arm64v8:
DOCKER_ARCH: arm64v8
# The default timeout of 60 minutes is a little low for compiling
# cryptography on ARM architectures.
timeoutInMinutes: 180
steps:
- bash: set -e && tools/docker/build.sh $(dockerTag) $DOCKER_ARCH
displayName: Build the Docker images
# We don't filter for the Docker Hub organization to continue to allow
# easy testing of these scripts on forks.
- bash: |
set -e
DOCKER_IMAGES=$(docker images --filter reference='*/certbot' --filter reference='*/dns-*' --format '{{.Repository}}')
docker save --output images.tar $DOCKER_IMAGES
displayName: Save the Docker images
# If the name of the tar file or artifact changes, the deploy stage will
# also need to be updated.
- bash: set -e && mv images.tar $(Build.ArtifactStagingDirectory)
displayName: Prepare Docker artifact
- task: PublishPipelineArtifact@1
inputs:
path: $(Build.ArtifactStagingDirectory)
artifact: docker_$(DOCKER_ARCH)
displayName: Store Docker artifact
- job: docker_run
dependsOn: docker_build
pool:
vmImage: ubuntu-18.04
steps:
- task: DownloadPipelineArtifact@2
inputs:
artifact: docker_amd64
path: $(Build.SourcesDirectory)
displayName: Retrieve Docker images
- bash: set -e && docker load --input $(Build.SourcesDirectory)/images.tar
displayName: Load Docker images
- bash: |
set -ex
DOCKER_IMAGES=$(docker images --filter reference='*/certbot' --filter reference='*/dns-*' --format '{{.Repository}}:{{.Tag}}')
for DOCKER_IMAGE in ${DOCKER_IMAGES}
do docker run --rm "${DOCKER_IMAGE}" plugins --prepare
done
displayName: Run integration tests for Docker images
- job: installer_build
pool:
vmImage: vs2017-win2016
steps:
- task: UsePythonVersion@0
inputs:
versionSpec: 3.8
architecture: x86
addToPath: true
- script: |
python -m venv venv
venv\Scripts\python tools\pipstrap.py
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
win2016:
imageName: vs2017-win2016
pool:
vmImage: $(imageName)
steps:
- powershell: |
if ($PSVersionTable.PSVersion.Major -ne 5) {
throw "Powershell version is not 5.x"
}
condition: eq(variables['imageName'], 'vs2017-win2016')
displayName: Check Powershell 5.x is used in vs2017-win2016
- task: UsePythonVersion@0
inputs:
versionSpec: 3.8
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\pipstrap.py
venv\Scripts\python tools\pip_install.py -e certbot-ci
env:
PIP_NO_BUILD_ISOLATION: no
displayName: Prepare Certbot-CI
- script: |
set PATH=%ProgramFiles(x86)%\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-win32.exe
displayName: Run windows installer integration tests
- script: |
set PATH=%ProgramFiles(x86)%\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-18.04
strategy:
matrix:
amd64:
SNAP_ARCH: amd64
# Do not run the heavy non-amd64 builds for test branches
${{ if not(startsWith(variables['Build.SourceBranchName'], 'test-')) }}:
armhf:
SNAP_ARCH: armhf
arm64:
SNAP_ARCH: arm64
timeoutInMinutes: 0
steps:
- script: |
set -e
sudo apt-get update
sudo apt-get install -y --no-install-recommends snapd
sudo snap install --classic snapcraft
displayName: Install dependencies
- task: UsePythonVersion@0
inputs:
versionSpec: 3.8
addToPath: true
- task: DownloadSecureFile@1
name: credentials
inputs:
secureFile: launchpad-credentials
- script: |
set -e
git config --global user.email "$(Build.RequestedForEmail)"
git config --global user.name "$(Build.RequestedFor)"
mkdir -p ~/.local/share/snapcraft/provider/launchpad
cp $(credentials.secureFilePath) ~/.local/share/snapcraft/provider/launchpad/credentials
python3 tools/snap/build_remote.py ALL --archs ${SNAP_ARCH} --timeout 19800
displayName: Build snaps
- script: |
set -e
mv *.snap $(Build.ArtifactStagingDirectory)
mv certbot-dns-*/*.snap $(Build.ArtifactStagingDirectory)
displayName: Prepare artifacts
- task: PublishPipelineArtifact@1
inputs:
path: $(Build.ArtifactStagingDirectory)
artifact: snaps_$(SNAP_ARCH)
displayName: Store snaps artifacts
- job: snap_run
dependsOn: snaps_build
pool:
vmImage: ubuntu-18.04
steps:
- task: UsePythonVersion@0
inputs:
versionSpec: 3.8
addToPath: true
- script: |
set -e
sudo apt-get update
sudo apt-get install -y --no-install-recommends nginx-light snapd
python3 -m venv venv
venv/bin/python tools/pipstrap.py
venv/bin/python tools/pip_install.py -U tox
displayName: Install dependencies
- task: DownloadPipelineArtifact@2
inputs:
artifact: snaps_amd64
path: $(Build.SourcesDirectory)/snap
displayName: Retrieve Certbot snaps
- script: |
set -e
sudo snap install --dangerous --classic snap/certbot_*.snap
displayName: Install Certbot snap
- script: |
set -e
venv/bin/python -m tox -e integration-external,apacheconftest-external-with-pebble
displayName: Run tox
- job: snap_dns_run
dependsOn: snaps_build
pool:
vmImage: ubuntu-18.04
steps:
- script: |
set -e
sudo apt-get update
sudo apt-get install -y --no-install-recommends snapd
displayName: Install dependencies
- task: UsePythonVersion@0
inputs:
versionSpec: 3.8
addToPath: true
- task: DownloadPipelineArtifact@2
inputs:
artifact: snaps_amd64
path: $(Build.SourcesDirectory)/snap
displayName: Retrieve Certbot snaps
- script: |
set -e
python3 -m venv venv
venv/bin/python tools/pipstrap.py
venv/bin/python tools/pip_install.py -e certbot-ci
displayName: Prepare Certbot-CI
- script: |
set -e
sudo -E venv/bin/pytest certbot-ci/snap_integration_tests/dns_tests --allow-persistent-changes --snap-folder $(Build.SourcesDirectory)/snap --snap-arch amd64
displayName: Test DNS plugins snaps

View File

@@ -1,80 +0,0 @@
jobs:
- job: test
variables:
PYTHON_VERSION: 3.9
strategy:
matrix:
macos-py36:
IMAGE_NAME: macOS-10.15
PYTHON_VERSION: 3.6
TOXENV: py36
macos-py39:
IMAGE_NAME: macOS-10.15
PYTHON_VERSION: 3.9
TOXENV: py39
windows-py36:
IMAGE_NAME: vs2017-win2016
PYTHON_VERSION: 3.6
TOXENV: py36
windows-py38-cover:
IMAGE_NAME: vs2017-win2016
PYTHON_VERSION: 3.8
TOXENV: py38-cover
windows-integration-certbot:
IMAGE_NAME: vs2017-win2016
PYTHON_VERSION: 3.8
TOXENV: integration-certbot
linux-oldest-tests-1:
IMAGE_NAME: ubuntu-18.04
PYTHON_VERSION: 3.6
TOXENV: '{acme,apache,apache-v2,certbot}-oldest'
linux-oldest-tests-2:
IMAGE_NAME: ubuntu-18.04
PYTHON_VERSION: 3.6
TOXENV: '{dns,nginx}-oldest'
linux-py36:
IMAGE_NAME: ubuntu-18.04
PYTHON_VERSION: 3.6
TOXENV: py36
linux-py39-cover:
IMAGE_NAME: ubuntu-18.04
PYTHON_VERSION: 3.9
TOXENV: py39-cover
linux-py39-lint:
IMAGE_NAME: ubuntu-18.04
PYTHON_VERSION: 3.9
TOXENV: lint
linux-py39-mypy:
IMAGE_NAME: ubuntu-18.04
PYTHON_VERSION: 3.9
TOXENV: mypy
linux-integration:
IMAGE_NAME: ubuntu-18.04
PYTHON_VERSION: 3.8
TOXENV: integration
ACME_SERVER: pebble
apache-compat:
IMAGE_NAME: ubuntu-18.04
TOXENV: apache_compat
# le-modification can be moved to the extended test suite once
# https://github.com/certbot/certbot/issues/8742 is resolved.
le-modification:
IMAGE_NAME: ubuntu-18.04
TOXENV: modification
apacheconftest:
IMAGE_NAME: ubuntu-18.04
PYTHON_VERSION: 3.6
TOXENV: apacheconftest-with-pebble
nginxroundtrip:
IMAGE_NAME: ubuntu-18.04
PYTHON_VERSION: 3.6
TOXENV: nginxroundtrip
pool:
vmImage: $(IMAGE_NAME)
steps:
- template: ../steps/tox-steps.yml
- job: test_sphinx_builds
pool:
vmImage: ubuntu-20.04
steps:
- template: ../steps/sphinx-steps.yml

View File

@@ -1,19 +0,0 @@
stages:
- stage: Changelog
jobs:
- job: prepare
pool:
vmImage: vs2017-win2016
steps:
# If we change the output filename from `release_notes.md`, it should also be changed in tools/create_github_release.py
- bash: |
set -e
CERTBOT_VERSION="$(cd certbot && python -c "import certbot; print(certbot.__version__)" && cd ~-)"
"${BUILD_REPOSITORY_LOCALPATH}\tools\extract_changelog.py" "${CERTBOT_VERSION}" >> "${BUILD_ARTIFACTSTAGINGDIRECTORY}/release_notes.md"
displayName: Prepare changelog
- 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: changelog
displayName: Publish changelog

View File

@@ -1,106 +0,0 @@
parameters:
- name: snapReleaseChannel
type: string
default: edge
values:
- edge
- beta
stages:
- stage: Deploy
jobs:
# This job relies on credentials used to publish the Certbot snaps. This
# credential file was created by running:
#
# snapcraft logout
# snapcraft login (provide the shared snapcraft credentials when prompted)
# snapcraft export-login --channels=beta,edge snapcraft.cfg
#
# Then the file was added as a secure file in Azure pipelines
# with the name snapcraft.cfg by following the instructions at
# https://docs.microsoft.com/en-us/azure/devops/pipelines/library/secure-files?view=azure-devops
# including authorizing the file in all pipelines as described at
# https://docs.microsoft.com/en-us/azure/devops/pipelines/library/secure-files?view=azure-devops#how-do-i-authorize-a-secure-file-for-use-in-all-pipelines.
#
# This file has a maximum lifetime of one year and the current
# file will expire on 2021-07-28 which is also tracked by
# https://github.com/certbot/certbot/issues/7931. The file will
# need to be updated before then to prevent automated deploys
# from breaking.
#
# Revoking these credentials can be done by changing the password of the
# account used to generate the credentials. See
# https://forum.snapcraft.io/t/revoking-exported-credentials/19031 for
# more info.
- job: publish_snap
pool:
vmImage: ubuntu-18.04
variables:
- group: certbot-common
strategy:
matrix:
amd64:
SNAP_ARCH: amd64
arm32v6:
SNAP_ARCH: armhf
arm64v8:
SNAP_ARCH: arm64
steps:
- bash: |
set -e
sudo apt-get update
sudo apt-get install -y --no-install-recommends snapd
sudo snap install --classic snapcraft
displayName: Install dependencies
- task: DownloadPipelineArtifact@2
inputs:
artifact: snaps_$(SNAP_ARCH)
path: $(Build.SourcesDirectory)/snap
displayName: Retrieve Certbot snaps
- task: DownloadSecureFile@1
name: snapcraftCfg
inputs:
secureFile: snapcraft.cfg
- bash: |
set -e
snapcraft login --with $(snapcraftCfg.secureFilePath)
for SNAP_FILE in snap/*.snap; do
tools/retry.sh eval snapcraft upload --release=${{ parameters.snapReleaseChannel }} "${SNAP_FILE}"
done
displayName: Publish to Snap store
- job: publish_docker
pool:
vmImage: ubuntu-18.04
strategy:
matrix:
amd64:
DOCKER_ARCH: amd64
arm32v6:
DOCKER_ARCH: arm32v6
arm64v8:
DOCKER_ARCH: arm64v8
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
# The credentials used here 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. "Grant access to all
# pipelines" should also be checked. To revoke these
# credentials, we can change the password on the certbotbot
# Docker Hub account or remove the account from the
# Certbot organization on Docker Hub.
containerRegistry: docker-hub
displayName: Login to Docker Hub
- bash: set -e && tools/docker/deploy.sh $(dockerTag) $DOCKER_ARCH
displayName: Deploy the Docker images

View File

@@ -1,19 +0,0 @@
stages:
- stage: On_Failure
jobs:
- job: notify_mattermost
variables:
- group: certbot-common
pool:
vmImage: ubuntu-20.04
steps:
- bash: |
set -e
MESSAGE="\
---\n\
##### Azure Pipeline
*Repo* $(Build.Repository.ID) - *Pipeline* $(Build.DefinitionName) #$(Build.BuildNumber) - *Branch/PR* $(Build.SourceBranchName)\n\
:warning: __Pipeline has failed__: [Link to the build](https://dev.azure.com/$(Build.Repository.ID)/_build/results?buildId=$(Build.BuildId)&view=results)\n\n\
---"
curl -i -X POST --data-urlencode "payload={\"text\":\"${MESSAGE}\"}" "$(MATTERMOST_URL)"
condition: failed()

View File

@@ -1,6 +0,0 @@
stages:
- stage: TestAndPackage
jobs:
- template: ../jobs/standard-tests-jobs.yml
- template: ../jobs/extended-tests-jobs.yml
- template: ../jobs/packaging-jobs.yml

View File

@@ -1,23 +0,0 @@
steps:
- bash: |
FINAL_STATUS=0
declare -a FAILED_BUILDS
python3 -m venv .venv
source .venv/bin/activate
python tools/pipstrap.py
for doc_path in */docs
do
echo ""
echo "##[group]Building $doc_path"
tools/pip_install_editable.py $doc_path/..[docs]
if ! sphinx-build -W --keep-going -b html $doc_path $doc_path/_build/html; then
FINAL_STATUS=1
FAILED_BUILDS[${#FAILED_BUILDS[@]}]="${doc_path%/docs}"
fi
echo "##[endgroup]"
done
if [[ $FINAL_STATUS -ne 0 ]]; then
echo "##[error]The following builds failed: ${FAILED_BUILDS[*]}"
exit 1
fi
displayName: Build Sphinx Documentation

View File

@@ -1,57 +0,0 @@
steps:
# We run brew update because we've seen attempts to install an older version
# of a package fail. See
# https://github.com/actions/virtual-environments/issues/3165.
- bash: |
set -e
brew update
brew install augeas
condition: startswith(variables['IMAGE_NAME'], 'macOS')
displayName: Install MacOS dependencies
- bash: |
set -e
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
python-dev \
gcc \
libaugeas0 \
libssl-dev \
libffi-dev \
ca-certificates \
nginx-light \
openssl
sudo systemctl stop nginx
condition: startswith(variables['IMAGE_NAME'], 'ubuntu')
displayName: Install Linux dependencies
- task: UsePythonVersion@0
inputs:
versionSpec: $(PYTHON_VERSION)
addToPath: true
# tools/pip_install.py is used to pin packages to a known working version
# except in tests where the environment variable CERTBOT_NO_PIN is set.
# virtualenv is listed here explicitly to make sure it is upgraded when
# CERTBOT_NO_PIN is set to work around failures we've seen when using an older
# version of virtualenv. The option "-I" is set so when CERTBOT_NO_PIN is also
# set, pip updates dependencies it thinks are already satisfied to avoid some
# problems with its lack of real dependency resolution.
- bash: |
set -e
python tools/pipstrap.py
python tools/pip_install.py -I tox virtualenv
displayName: Install runtime dependencies
- task: DownloadSecureFile@1
name: testFarmPem
inputs:
secureFile: azure-test-farm.pem
condition: contains(variables['TOXENV'], 'test-farm')
- bash: |
set -e
export TARGET_BRANCH="`echo "${BUILD_SOURCEBRANCH}" | sed -E 's!refs/(heads|tags)/!!g'`"
[ -z "${SYSTEM_PULLREQUEST_TARGETBRANCH}" ] || export TARGET_BRANCH="${SYSTEM_PULLREQUEST_TARGETBRANCH}"
env
python -m tox
env:
AWS_ACCESS_KEY_ID: $(AWS_ACCESS_KEY_ID)
AWS_SECRET_ACCESS_KEY: $(AWS_SECRET_ACCESS_KEY)
AWS_EC2_PEM_FILE: $(testFarmPem.secureFilePath)
displayName: Run tox

View File

@@ -0,0 +1,52 @@
jobs:
- job: test
strategy:
matrix:
macos-py27:
IMAGE_NAME: macOS-10.14
PYTHON_VERSION: 2.7
TOXENV: py27
macos-py38:
IMAGE_NAME: macOS-10.14
PYTHON_VERSION: 3.8
TOXENV: py38
windows-py35:
IMAGE_NAME: vs2017-win2016
PYTHON_VERSION: 3.5
TOXENV: py35
windows-py37-cover:
IMAGE_NAME: vs2017-win2016
PYTHON_VERSION: 3.7
TOXENV: py37-cover
windows-integration-certbot:
IMAGE_NAME: vs2017-win2016
PYTHON_VERSION: 3.7
TOXENV: integration-certbot
PYTEST_ADDOPTS: --numprocesses 4
pool:
vmImage: $(IMAGE_NAME)
variables:
- group: certbot-common
steps:
- bash: brew install augeas
condition: startswith(variables['IMAGE_NAME'], 'macOS')
displayName: Install Augeas
- task: UsePythonVersion@0
inputs:
versionSpec: $(PYTHON_VERSION)
addToPath: true
- script: python tools/pip_install.py -U tox coverage
displayName: Install dependencies
- script: python -m tox
displayName: Run tox
# We do not require codecov report upload to succeed. So to avoid to break the pipeline if
# something goes wrong, each command is suffixed with a command that hides any non zero exit
# codes and echoes an informative message instead.
- bash: |
curl -s https://codecov.io/bash -o codecov-bash || echo "Failed to download codecov-bash"
chmod +x codecov-bash || echo "Failed to apply execute permissions on codecov-bash"
./codecov-bash -F windows || echo "Codecov did not collect coverage reports"
condition: in(variables['TOXENV'], 'py37-cover', 'integration-certbot')
env:
CODECOV_TOKEN: $(codecov_token)
displayName: Publish coverage

18
.codecov.yml Normal file
View File

@@ -0,0 +1,18 @@
coverage:
status:
project:
default: off
linux:
flags: linux
# Fixed target instead of auto set by #7173, can
# be removed when flags in Codecov are added back.
target: 97.4
threshold: 0.1
base: auto
windows:
flags: windows
# Fixed target instead of auto set by #7173, can
# be removed when flags in Codecov are added back.
target: 97.4
threshold: 0.1
base: auto

View File

@@ -8,4 +8,5 @@
.git
.tox
venv
venv3
docs

View File

@@ -1,18 +0,0 @@
# https://editorconfig.org/
root = true
[*]
insert_final_newline = true
trim_trailing_whitespace = true
end_of_line = lf
[*.py]
indent_style = space
indent_size = 4
charset = utf-8
max_line_length = 100
[*.yaml]
indent_style = space
indent_size = 2

12
.envrc
View File

@@ -1,12 +0,0 @@
# This file is just a nicety for developers who use direnv. When you cd under
# the Certbot repo, Certbot's virtual environment will be automatically
# activated and then deactivated when you cd elsewhere. Developers have to have
# direnv set up and run `direnv allow` to allow this file to execute on their
# system. You can find more information at https://direnv.net/.
. venv/bin/activate
# direnv doesn't support modifying PS1 so we unset it to squelch the error
# it'll otherwise print about this being done in the activate script. See
# https://github.com/direnv/direnv/wiki/PS1. If you would like your shell
# prompt to change like it normally does, see
# https://github.com/direnv/direnv/wiki/Python#restoring-the-ps1.
unset PS1

1
.github/FUNDING.yml vendored
View File

@@ -1 +0,0 @@
custom: https://supporters.eff.org/donate/support-work-on-certbot

22
.gitignore vendored
View File

@@ -4,12 +4,13 @@
build/
dist*/
/venv*/
/kgs/
/.tox/
/releases*/
/log*
letsencrypt.log
certbot.log
poetry.lock
letsencrypt-auto-source/letsencrypt-auto.sig.lzma.base64
# coverage
.coverage
@@ -30,6 +31,11 @@ tags
# auth --cert-path --chain-path
/*.pem
# letstest
tests/letstest/letest-*/
tests/letstest/*.pem
tests/letstest/venv/
.venv
# pytest cache
@@ -44,17 +50,3 @@ tags
.certbot_test_workspace
**/assets/pebble*
**/assets/challtestsrv*
# snap files
.snapcraft
parts
prime
stage
*.snap
snap-constraints.txt
qemu-*
certbot-dns*/certbot-dns*_amd64*.txt
certbot-dns*/certbot-dns*_arm*.txt
/certbot_amd64*.txt
/certbot_arm*.txt
certbot-dns*/snap

View File

@@ -1,5 +1,6 @@
[settings]
skip_glob=venv*
skip=letsencrypt-auto-source
force_sort_within_sections=True
force_single_line=True
order_by_type=False

View File

@@ -8,10 +8,7 @@ jobs=0
# Python code to execute, usually for sys.path manipulation such as
# pygtk.require().
# 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(pylint.config.PYLINTRC))"
#init-hook=
# Profiled execution.
profile=no
@@ -56,18 +53,7 @@ extension-pkg-whitelist=pywintypes,win32api,win32file,win32security
# See https://github.com/PyCQA/pylint/issues/1498.
# 3) Same as point 2 for no-value-for-parameter.
# See https://github.com/PyCQA/pylint/issues/2820.
# 4) raise-missing-from makes it an error to raise an exception from except
# block without using explicit exception chaining. While explicit exception
# chaining results in a slightly more informative traceback, I don't think
# it's beneficial enough for us to change all of our current instances and
# give Certbot developers errors about this when they're working on new code
# in the future. You can read more about exception chaining and this pylint
# check at
# https://blog.ram.rachum.com/post/621791438475296768/improving-python-exception-chaining-with.
# 5) wrong-import-order generates false positives and a pylint developer
# suggests that people using isort should disable this check at
# https://github.com/PyCQA/pylint/issues/3817#issuecomment-687892090.
disable=fixme,locally-disabled,locally-enabled,bad-continuation,no-self-use,invalid-name,cyclic-import,duplicate-code,design,import-outside-toplevel,useless-object-inheritance,unsubscriptable-object,no-value-for-parameter,no-else-return,no-else-raise,no-else-break,no-else-continue,raise-missing-from,wrong-import-order
disable=fixme,locally-disabled,locally-enabled,bad-continuation,no-self-use,invalid-name,cyclic-import,duplicate-code,design,import-outside-toplevel,useless-object-inheritance,unsubscriptable-object,no-value-for-parameter,no-else-return,no-else-raise,no-else-break,no-else-continue
[REPORTS]
@@ -268,7 +254,7 @@ ignore-mixin-members=yes
# 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=pkg_resources,confargparse,argparse
ignored-modules=pkg_resources,confargparse,argparse,six.moves,six.moves.urllib
# import errors ignored only in 1.4.4
# https://bitbucket.org/logilab/pylint/commits/cd000904c9e2

271
.travis.yml Normal file
View File

@@ -0,0 +1,271 @@
language: python
dist: xenial
cache:
directories:
- $HOME/.cache/pip
before_script:
# On Travis, the fastest parallelization for integration tests has proved to be 4.
- 'if [[ "$TOXENV" == *"integration"* ]]; then export PYTEST_ADDOPTS="--numprocesses 4"; fi'
# Use Travis retry feature for farm tests since they are flaky
- 'if [[ "$TOXENV" == "travis-test-farm"* ]]; then export TRAVIS_RETRY=travis_retry; fi'
- export TOX_TESTENV_PASSENV=TRAVIS
# Only build pushes to the master branch, PRs, and branches beginning with
# `test-`, `travis-test-`, or of the form `digit(s).digit(s).x`. This reduces
# the number of simultaneous Travis runs, which speeds turnaround time on
# review since there is a cap of on the number of simultaneous runs.
branches:
# When changing these branches, please ensure the documentation under
# "Running tests in CI" is still correct.
only:
# apache-parser-v2 is a temporary branch for doing work related to
# rewriting the parser in the Apache plugin.
- apache-parser-v2
- master
- /^\d+\.\d+\.x$/
- /^(travis-)?test-.*$/
# Jobs for the main test suite are always executed (including on PRs) except for pushes on master.
not-on-master: &not-on-master
if: NOT (type = push AND branch = master)
# Jobs for the extended test suite are executed for cron jobs and pushes to
# non-development branches. See the explanation for apache-parser-v2 above.
extended-test-suite: &extended-test-suite
if: type = cron OR (type = push AND branch NOT IN (apache-parser-v2, master))
matrix:
include:
# Main test suite
- python: "2.7"
env: ACME_SERVER=pebble TOXENV=integration
<<: *not-on-master
# This job is always executed, including on master
- python: "2.7"
env: TOXENV=py27-cover FYI="py27 tests + code coverage"
- python: "3.7"
env: TOXENV=lint
<<: *not-on-master
- python: "3.5"
env: TOXENV=mypy
<<: *not-on-master
- python: "2.7"
# Ubuntu Trusty or older must be used because the oldest version of
# cryptography we support cannot be compiled against the version of
# OpenSSL in Xenial or newer.
dist: trusty
env: TOXENV='py27-{acme,apache,apache-v2,certbot,dns,nginx}-oldest'
<<: *not-on-master
- python: "3.5"
env: TOXENV=py35
<<: *not-on-master
- python: "3.8"
env: TOXENV=py38
<<: *not-on-master
- sudo: required
env: TOXENV=apache_compat
services: docker
before_install:
addons:
<<: *not-on-master
- sudo: required
env: TOXENV=le_auto_xenial
services: docker
<<: *not-on-master
- python: "2.7"
env: TOXENV=apacheconftest-with-pebble
<<: *not-on-master
- python: "2.7"
env: TOXENV=nginxroundtrip
<<: *not-on-master
# Extended test suite on cron jobs and pushes to tested branches other than master
- sudo: required
env: TOXENV=nginx_compat
services: docker
before_install:
addons:
<<: *extended-test-suite
- python: "2.7"
env:
- TOXENV=travis-test-farm-apache2
- secure: "f+j/Lj9s1lcuKo5sEFrlRd1kIAMnIJI4z0MTI7QF8jl9Fkmbx7KECGzw31TNgzrOSzxSapHbcueFYvNCLKST+kE/8ogMZBbwqXfEDuKpyF6BY3uYoJn+wPVE5pIb8Hhe08xPte8TTDSMIyHI3EyTfcAKrIreauoArePvh/cRvSw="
<<: *extended-test-suite
- python: "2.7"
env:
- TOXENV=travis-test-farm-leauto-upgrades
- secure: "f+j/Lj9s1lcuKo5sEFrlRd1kIAMnIJI4z0MTI7QF8jl9Fkmbx7KECGzw31TNgzrOSzxSapHbcueFYvNCLKST+kE/8ogMZBbwqXfEDuKpyF6BY3uYoJn+wPVE5pIb8Hhe08xPte8TTDSMIyHI3EyTfcAKrIreauoArePvh/cRvSw="
git:
depth: false # This is needed to have the history to checkout old versions of certbot-auto.
<<: *extended-test-suite
- python: "2.7"
env:
- TOXENV=travis-test-farm-certonly-standalone
- secure: "f+j/Lj9s1lcuKo5sEFrlRd1kIAMnIJI4z0MTI7QF8jl9Fkmbx7KECGzw31TNgzrOSzxSapHbcueFYvNCLKST+kE/8ogMZBbwqXfEDuKpyF6BY3uYoJn+wPVE5pIb8Hhe08xPte8TTDSMIyHI3EyTfcAKrIreauoArePvh/cRvSw="
<<: *extended-test-suite
- python: "2.7"
env:
- TOXENV=travis-test-farm-sdists
- secure: "f+j/Lj9s1lcuKo5sEFrlRd1kIAMnIJI4z0MTI7QF8jl9Fkmbx7KECGzw31TNgzrOSzxSapHbcueFYvNCLKST+kE/8ogMZBbwqXfEDuKpyF6BY3uYoJn+wPVE5pIb8Hhe08xPte8TTDSMIyHI3EyTfcAKrIreauoArePvh/cRvSw="
<<: *extended-test-suite
- python: "3.7"
env: TOXENV=py37 CERTBOT_NO_PIN=1
<<: *extended-test-suite
- python: "2.7"
env: ACME_SERVER=boulder-v1 TOXENV=integration
sudo: required
services: docker
<<: *extended-test-suite
- python: "2.7"
env: ACME_SERVER=boulder-v2 TOXENV=integration
sudo: required
services: docker
<<: *extended-test-suite
- python: "2.7"
env: ACME_SERVER=boulder-v1 TOXENV=integration-certbot-oldest
# Ubuntu Trusty or older must be used because the oldest version of
# cryptography we support cannot be compiled against the version of
# OpenSSL in Xenial or newer.
dist: trusty
sudo: required
services: docker
<<: *extended-test-suite
- python: "2.7"
env: ACME_SERVER=boulder-v2 TOXENV=integration-certbot-oldest
# Ubuntu Trusty or older must be used because the oldest version of
# cryptography we support cannot be compiled against the version of
# OpenSSL in Xenial or newer.
dist: trusty
sudo: required
services: docker
<<: *extended-test-suite
- python: "2.7"
env: ACME_SERVER=boulder-v1 TOXENV=integration-nginx-oldest
# Ubuntu Trusty or older must be used because the oldest version of
# cryptography we support cannot be compiled against the version of
# OpenSSL in Xenial or newer.
dist: trusty
sudo: required
services: docker
<<: *extended-test-suite
- python: "2.7"
env: ACME_SERVER=boulder-v2 TOXENV=integration-nginx-oldest
# Ubuntu Trusty or older must be used because the oldest version of
# cryptography we support cannot be compiled against the version of
# OpenSSL in Xenial or newer.
dist: trusty
sudo: required
services: docker
<<: *extended-test-suite
- python: "3.6"
env: TOXENV=py36
<<: *extended-test-suite
- python: "3.7"
env: TOXENV=py37
<<: *extended-test-suite
- python: "3.5"
env: ACME_SERVER=boulder-v1 TOXENV=integration
sudo: required
services: docker
<<: *extended-test-suite
- python: "3.5"
env: ACME_SERVER=boulder-v2 TOXENV=integration
sudo: required
services: docker
<<: *extended-test-suite
- python: "3.6"
env: ACME_SERVER=boulder-v1 TOXENV=integration
sudo: required
services: docker
<<: *extended-test-suite
- python: "3.6"
env: ACME_SERVER=boulder-v2 TOXENV=integration
sudo: required
services: docker
<<: *extended-test-suite
- python: "3.7"
env: ACME_SERVER=boulder-v1 TOXENV=integration
sudo: required
services: docker
<<: *extended-test-suite
- python: "3.7"
env: ACME_SERVER=boulder-v2 TOXENV=integration
sudo: required
services: docker
<<: *extended-test-suite
- python: "3.8"
env: ACME_SERVER=boulder-v1 TOXENV=integration
<<: *extended-test-suite
- python: "3.8"
env: ACME_SERVER=boulder-v2 TOXENV=integration
<<: *extended-test-suite
- sudo: required
env: TOXENV=le_auto_jessie
services: docker
<<: *extended-test-suite
- sudo: required
env: TOXENV=le_auto_centos6
services: docker
<<: *extended-test-suite
- sudo: required
env: TOXENV=le_auto_oraclelinux6
services: docker
<<: *extended-test-suite
- sudo: required
env: TOXENV=docker_dev
services: docker
addons:
apt:
packages: # don't install nginx and apache
- libaugeas0
<<: *extended-test-suite
# container-based infrastructure
sudo: false
addons:
apt:
packages: # Keep in sync with letsencrypt-auto-source/pieces/bootstrappers/deb_common.sh and Boulder.
- python-dev
- gcc
- libaugeas0
- libssl-dev
- libffi-dev
- ca-certificates
# For certbot-nginx integration testing
- nginx-light
- openssl
# tools/pip_install.py is used to pin packages to a known working version
# except in tests where the environment variable CERTBOT_NO_PIN is set.
# virtualenv is listed here explicitly to make sure it is upgraded when
# CERTBOT_NO_PIN is set to work around failures we've seen when using an older
# version of virtualenv. The option "-I" is set so when CERTBOT_NO_PIN is also
# set, pip updates dependencies it thinks are already satisfied to avoid some
# problems with its lack of real dependency resolution.
install: 'tools/pip_install.py -I codecov tox virtualenv'
# Most of the time TRAVIS_RETRY is an empty string, and has no effect on the
# script command. It is set only to `travis_retry` during farm tests, in
# order to trigger the Travis retry feature, and compensate the inherent
# flakiness of these specific tests.
script: '$TRAVIS_RETRY tox'
after_success: '[ "$TOXENV" == "py27-cover" ] && codecov -F linux'
notifications:
email: false
irc:
if: NOT branch =~ ^(travis-)?test-.*$
channels:
# This is set to a secure variable to prevent forks from sending
# notifications. This value was created by installing
# https://github.com/travis-ci/travis.rb and running
# `travis encrypt "chat.freenode.net#certbot-devel"`.
- secure: "EWW66E2+KVPZyIPR8ViENZwfcup4Gx3/dlimmAZE0WuLwxDCshBBOd3O8Rf6pBokEoZlXM5eDT6XdyJj8n0DLslgjO62pExdunXpbcMwdY7l1ELxX2/UbnDTE6UnPYa09qVBHNG7156Z6yE0x2lH4M9Ykvp0G0cubjPQHylAwo0="
on_cancel: never
on_success: never
on_failure: always

View File

@@ -1,7 +1,6 @@
Authors
=======
* [Aaron Gable](https://github.com/aarongable)
* [Aaron Zirbes](https://github.com/aaronzirbes)
* Aaron Zuehlke
* Ada Lovelace
@@ -22,7 +21,6 @@ Authors
* [Andrzej Górski](https://github.com/andrzej3393)
* [Anselm Levskaya](https://github.com/levskaya)
* [Antoine Jacoutot](https://github.com/ajacoutot)
* [April King](https://github.com/april)
* [asaph](https://github.com/asaph)
* [Axel Beckert](https://github.com/xtaran)
* [Bas](https://github.com/Mechazawa)
@@ -37,8 +35,7 @@ Authors
* [Blake Griffith](https://github.com/cowlicks)
* [Brad Warren](https://github.com/bmw)
* [Brandon Kraft](https://github.com/kraftbj)
* [Brandon Kreisel](https://github.com/BKreisel)
* [Brian Heim](https://github.com/brianlheim)
* [Brandon Kreisel](https://github.com/kraftbj)
* [Cameron Steel](https://github.com/Tugzrida)
* [Ceesjan Luiten](https://github.com/quinox)
* [Chad Whitacre](https://github.com/whit537)
@@ -61,9 +58,7 @@ Authors
* [DanCld](https://github.com/DanCld)
* [Daniel Albers](https://github.com/AID)
* [Daniel Aleksandersen](https://github.com/da2x)
* [Daniel Almasi](https://github.com/almasen)
* [Daniel Convissor](https://github.com/convissor)
* [Daniel "Drex" Drexler](https://github.com/aeturnum)
* [Daniel Huang](https://github.com/dhuang)
* [Dave Guarino](https://github.com/daguar)
* [David cz](https://github.com/dave-cz)
@@ -88,7 +83,6 @@ Authors
* [Felix Schwarz](https://github.com/FelixSchwarz)
* [Felix Yan](https://github.com/felixonmars)
* [Filip Ochnik](https://github.com/filipochnik)
* [Florian Klink](https://github.com/flokli)
* [Francois Marier](https://github.com/fmarier)
* [Frank](https://github.com/Frankkkkk)
* [Frederic BLANC](https://github.com/fblanc)
@@ -109,7 +103,6 @@ Authors
* [Henry Chen](https://github.com/henrychen95)
* [Hugo van Kemenade](https://github.com/hugovk)
* [Ingolf Becker](https://github.com/watercrossing)
* [Ivan Nejgebauer](https://github.com/inejge)
* [Jaap Eldering](https://github.com/eldering)
* [Jacob Hoffman-Andrews](https://github.com/jsha)
* [Jacob Sachs](https://github.com/jsachs)
@@ -151,13 +144,11 @@ Authors
* [Lior Sabag](https://github.com/liorsbg)
* [Lipis](https://github.com/lipis)
* [lord63](https://github.com/lord63)
* [Lorenzo Fundaró](https://github.com/lfundaro)
* [Luca Beltrame](https://github.com/lbeltrame)
* [Luca Ebach](https://github.com/lucebac)
* [Luca Olivetti](https://github.com/olivluca)
* [Luke Rogers](https://github.com/lukeroge)
* [Maarten](https://github.com/mrtndwrd)
* [Mads Jensen](https://github.com/atombrella)
* [Maikel Martens](https://github.com/krukas)
* [Malte Janduda](https://github.com/MalteJ)
* [Mantas Mikulėnas](https://github.com/grawity)
@@ -209,7 +200,6 @@ Authors
* [Pierre Jaury](https://github.com/kaiyou)
* [Piotr Kasprzyk](https://github.com/kwadrat)
* [Prayag Verma](https://github.com/pra85)
* [Rasesh Patel](https://github.com/raspat1)
* [Reinaldo de Souza Jr](https://github.com/juniorz)
* [Remi Rampin](https://github.com/remram44)
* [Rémy HUBSCHER](https://github.com/Natim)
@@ -217,7 +207,6 @@ Authors
* [Richard Barnes](https://github.com/r-barnes)
* [Richard Panek](https://github.com/kernelpanek)
* [Robert Buchholz](https://github.com/rbu)
* [Robert Dailey](https://github.com/pahrohfit)
* [Robert Habermann](https://github.com/frennkie)
* [Robert Xiao](https://github.com/nneonneo)
* [Roland Shoemaker](https://github.com/rolandshoemaker)
@@ -243,11 +232,9 @@ Authors
* [Spencer Bliven](https://github.com/sbliven)
* [Stacey Sheldon](https://github.com/solidgoldbomb)
* [Stavros Korokithakis](https://github.com/skorokithakis)
* [Ștefan Talpalaru](https://github.com/stefantalpalaru)
* [Stefan Weil](https://github.com/stweil)
* [Steve Desmond](https://github.com/stevedesmond-ca)
* [sydneyli](https://github.com/sydneyli)
* [taixx046](https://github.com/taixx046)
* [Tan Jay Jun](https://github.com/jayjun)
* [Tapple Gao](https://github.com/tapple)
* [Telepenin Nikolay](https://github.com/telepenin)
@@ -279,6 +266,5 @@ Authors
* [Yomna](https://github.com/ynasser)
* [Yoni Jah](https://github.com/yonjah)
* [YourDaddyIsHere](https://github.com/YourDaddyIsHere)
* [Yuseong Cho](https://github.com/g6123)
* [Zach Shepherd](https://github.com/zjs)
* [陈三](https://github.com/chenxsan)

View File

@@ -11,7 +11,7 @@ to the Sphinx generated docs is provided below.
[1] https://github.com/blog/1184-contributing-guidelines
[2] https://docutils.sourceforge.io/docs/user/rst/quickref.html#hyperlink-targets
[2] http://docutils.sourceforge.net/docs/user/rst/quickref.html#hyperlink-targets
-->

View File

@@ -1,5 +1,5 @@
# This Dockerfile builds an image for development.
FROM ubuntu:focal
FROM debian:buster
# Note: this only exposes the port to other docker containers.
EXPOSE 80 443
@@ -8,14 +8,13 @@ WORKDIR /opt/certbot/src
COPY . .
RUN apt-get update && \
DEBIAN_FRONTEND=noninteractive apt-get install apache2 git python3-dev \
python3-venv gcc libaugeas0 libssl-dev libffi-dev ca-certificates \
openssl nginx-light -y --no-install-recommends && \
apt-get install apache2 git python3-dev python3-venv gcc libaugeas0 \
libssl-dev libffi-dev ca-certificates openssl nginx-light -y && \
apt-get clean && \
rm -rf /var/lib/apt/lists/* \
/tmp/* \
/var/tmp/*
RUN VENV_NAME="../venv" python3 tools/venv.py
RUN VENV_NAME="../venv3" python3 tools/venv3.py
ENV PATH /opt/certbot/venv/bin:$PATH
ENV PATH /opt/certbot/venv3/bin:$PATH

View File

@@ -7,7 +7,7 @@ questions.
## My operating system is (include version):
## I installed Certbot with (snap, OS package manager, pip, certbot-auto, etc):
## I installed Certbot with (certbot-auto, OS package manager, pip, etc):
## I ran this command and it produced this output:

View File

@@ -6,6 +6,7 @@ This module is an implementation of the `ACME protocol`_.
"""
import sys
import warnings
# This code exists to keep backwards compatibility with people using acme.jose
# before it became the standalone josepy package.

View File

@@ -1,23 +1,15 @@
"""ACME Identifier Validation Challenges."""
import abc
import codecs
import functools
import hashlib
import logging
import socket
from typing import Type
from cryptography.hazmat.primitives import hashes # type: ignore
import josepy as jose
from OpenSSL import crypto
from OpenSSL import SSL # type: ignore # https://github.com/python/typeshed/issues/2052
import requests
import six
from acme import crypto_util
from acme import errors
from acme import fields
from acme.mixins import ResourceMixin
from acme.mixins import TypeMixin
logger = logging.getLogger(__name__)
@@ -25,21 +17,21 @@ logger = logging.getLogger(__name__)
class Challenge(jose.TypedJSONObjectWithFields):
# _fields_to_partial_json
"""ACME challenge."""
TYPES: dict = {}
TYPES = {} # type: dict
@classmethod
def from_json(cls, jobj):
try:
return super().from_json(jobj)
return super(Challenge, cls).from_json(jobj)
except jose.UnrecognizedTypeError as error:
logger.debug(error)
return UnrecognizedChallenge.from_json(jobj)
class ChallengeResponse(ResourceMixin, TypeMixin, jose.TypedJSONObjectWithFields):
class ChallengeResponse(jose.TypedJSONObjectWithFields):
# _fields_to_partial_json
"""ACME challenge response."""
TYPES: dict = {}
TYPES = {} # type: dict
resource_type = 'challenge'
resource = fields.Resource(resource_type)
@@ -58,7 +50,7 @@ class UnrecognizedChallenge(Challenge):
"""
def __init__(self, jobj):
super().__init__()
super(UnrecognizedChallenge, self).__init__()
object.__setattr__(self, "jobj", jobj)
def to_partial_json(self):
@@ -141,20 +133,21 @@ class KeyAuthorizationChallengeResponse(ChallengeResponse):
return True
def to_partial_json(self):
jobj = super().to_partial_json()
jobj = super(KeyAuthorizationChallengeResponse, self).to_partial_json()
jobj.pop('keyAuthorization', None)
return jobj
class KeyAuthorizationChallenge(_TokenChallenge, metaclass=abc.ABCMeta):
@six.add_metaclass(abc.ABCMeta)
class KeyAuthorizationChallenge(_TokenChallenge):
"""Challenge based on Key Authorization.
:param response_cls: Subclass of `KeyAuthorizationChallengeResponse`
that will be used to generate ``response``.
that will be used to generate `response`.
:param str typ: type of the challenge
"""
typ: str = NotImplemented
response_cls: Type[KeyAuthorizationChallengeResponse] = NotImplemented
typ = NotImplemented
response_cls = NotImplemented
thumbprint_hash_function = (
KeyAuthorizationChallengeResponse.thumbprint_hash_function)
@@ -369,163 +362,29 @@ class HTTP01(KeyAuthorizationChallenge):
@ChallengeResponse.register
class TLSALPN01Response(KeyAuthorizationChallengeResponse):
"""ACME tls-alpn-01 challenge response."""
"""ACME TLS-ALPN-01 challenge response.
This class only allows initiating a TLS-ALPN-01 challenge returned from the
CA. Full support for responding to TLS-ALPN-01 challenges by generating and
serving the expected response certificate is not currently provided.
"""
typ = "tls-alpn-01"
PORT = 443
"""Verification port as defined by the protocol.
You can override it (e.g. for testing) by passing ``port`` to
`simple_verify`.
@Challenge.register
class TLSALPN01(KeyAuthorizationChallenge):
"""ACME tls-alpn-01 challenge.
This class simply allows parsing the TLS-ALPN-01 challenge returned from
the CA. Full TLS-ALPN-01 support is not currently provided.
"""
ID_PE_ACME_IDENTIFIER_V1 = b"1.3.6.1.5.5.7.1.30.1"
ACME_TLS_1_PROTOCOL = "acme-tls/1"
@property
def h(self):
"""Hash value stored in challenge certificate"""
return hashlib.sha256(self.key_authorization.encode('utf-8')).digest()
def gen_cert(self, domain, key=None, bits=2048):
"""Generate tls-alpn-01 certificate.
:param unicode domain: Domain verified by the challenge.
:param OpenSSL.crypto.PKey key: Optional private key used in
certificate generation. If not provided (``None``), then
fresh key will be generated.
:param int bits: Number of bits for newly generated key.
:rtype: `tuple` of `OpenSSL.crypto.X509` and `OpenSSL.crypto.PKey`
"""
if key is None:
key = crypto.PKey()
key.generate_key(crypto.TYPE_RSA, bits)
der_value = b"DER:" + codecs.encode(self.h, 'hex')
acme_extension = crypto.X509Extension(self.ID_PE_ACME_IDENTIFIER_V1,
critical=True, value=der_value)
return crypto_util.gen_ss_cert(key, [domain], force_san=True,
extensions=[acme_extension]), key
def probe_cert(self, domain, host=None, port=None):
"""Probe tls-alpn-01 challenge certificate.
:param unicode domain: domain being validated, required.
:param string host: IP address used to probe the certificate.
:param int port: Port used to probe the certificate.
"""
if host is None:
host = socket.gethostbyname(domain)
logger.debug('%s resolved to %s', domain, host)
if port is None:
port = self.PORT
return crypto_util.probe_sni(host=host, port=port, name=domain,
alpn_protocols=[self.ACME_TLS_1_PROTOCOL])
def verify_cert(self, domain, cert):
"""Verify tls-alpn-01 challenge certificate.
:param unicode domain: Domain name being validated.
:param OpensSSL.crypto.X509 cert: Challenge certificate.
:returns: Whether the certificate was successfully verified.
:rtype: bool
"""
# pylint: disable=protected-access
names = crypto_util._pyopenssl_cert_or_req_all_names(cert)
logger.debug('Certificate %s. SANs: %s', cert.digest('sha256'), names)
if len(names) != 1 or names[0].lower() != domain.lower():
return False
for i in range(cert.get_extension_count()):
ext = cert.get_extension(i)
# FIXME: assume this is the ACME extension. Currently there is no
# way to get full OID of an unknown extension from pyopenssl.
if ext.get_short_name() == b'UNDEF':
data = ext.get_data()
return data == self.h
return False
# pylint: disable=too-many-arguments
def simple_verify(self, chall, domain, account_public_key,
cert=None, host=None, port=None):
"""Simple verify.
Verify ``validation`` using ``account_public_key``, optionally
probe tls-alpn-01 certificate and check using `verify_cert`.
:param .challenges.TLSALPN01 chall: Corresponding challenge.
:param str domain: Domain name being validated.
:param JWK account_public_key:
:param OpenSSL.crypto.X509 cert: Optional certificate. If not
provided (``None``) certificate will be retrieved using
`probe_cert`.
:param string host: IP address used to probe the certificate.
:param int port: Port used to probe the certificate.
:returns: ``True`` if and only if client's control of the domain has been verified.
:rtype: bool
"""
if not self.verify(chall, account_public_key):
logger.debug("Verification of key authorization in response failed")
return False
if cert is None:
try:
cert = self.probe_cert(domain=domain, host=host, port=port)
except errors.Error as error:
logger.debug(str(error), exc_info=True)
return False
return self.verify_cert(domain, cert)
@Challenge.register # pylint: disable=too-many-ancestors
class TLSALPN01(KeyAuthorizationChallenge):
"""ACME tls-alpn-01 challenge."""
typ = "tls-alpn-01"
response_cls = TLSALPN01Response
typ = response_cls.typ
def validation(self, account_key, **kwargs):
"""Generate validation.
:param JWK account_key:
:param unicode domain: Domain verified by the challenge.
:param OpenSSL.crypto.PKey cert_key: Optional private key used
in certificate generation. If not provided (``None``), then
fresh key will be generated.
:rtype: `tuple` of `OpenSSL.crypto.X509` and `OpenSSL.crypto.PKey`
"""
return self.response(account_key).gen_cert(
key=kwargs.get('cert_key'),
domain=kwargs.get('domain'))
@staticmethod
def is_supported():
"""
Check if TLS-ALPN-01 challenge is supported on this machine.
This implies that a recent version of OpenSSL is installed (>= 1.0.2),
or a recent cryptography version shipped with the OpenSSL library is installed.
:returns: ``True`` if TLS-ALPN-01 is supported on this machine, ``False`` otherwise.
:rtype: bool
"""
return (hasattr(SSL.Connection, "set_alpn_protos")
and hasattr(SSL.Context, "set_alpn_select_callback"))
"""Generate validation for the challenge."""
raise NotImplementedError()
@Challenge.register

View File

@@ -4,38 +4,47 @@ import collections
import datetime
from email.utils import parsedate_tz
import heapq
import http.client as http_client
import logging
import re
import sys
import time
from typing import cast
from typing import Dict
from typing import List
from typing import Set
from typing import Text
from typing import Union
import josepy as jose
import OpenSSL
import requests
from requests.adapters import HTTPAdapter
from requests.utils import parse_header_links
from requests_toolbelt.adapters.source import SourceAddressAdapter
import six
from six.moves import http_client
from acme import crypto_util
from acme import errors
from acme import jws
from acme import messages
from acme.mixins import VersionedLEACMEMixin
from acme.magic_typing import Dict
from acme.magic_typing import List
from acme.magic_typing import Set
from acme.magic_typing import Text
logger = logging.getLogger(__name__)
# Prior to Python 2.7.9 the stdlib SSL module did not allow a user to configure
# many important security related options. On these platforms we use PyOpenSSL
# for SSL, which does allow these options to be configured.
# https://urllib3.readthedocs.org/en/latest/security.html#insecureplatformwarning
if sys.version_info < (2, 7, 9): # pragma: no cover
try:
requests.packages.urllib3.contrib.pyopenssl.inject_into_urllib3() # type: ignore
except AttributeError:
import urllib3.contrib.pyopenssl
urllib3.contrib.pyopenssl.inject_into_urllib3()
DEFAULT_NETWORK_TIMEOUT = 45
DER_CONTENT_TYPE = 'application/pkix-cert'
class ClientBase:
class ClientBase(object):
"""ACME client base object.
:ivar messages.Directory directory:
@@ -114,9 +123,8 @@ class ClientBase:
"""
return self.update_registration(regr, update={'status': 'deactivated'})
def deactivate_authorization(self,
authzr: messages.AuthorizationResource
) -> messages.AuthorizationResource:
def deactivate_authorization(self, authzr):
# type: (messages.AuthorizationResource) -> messages.AuthorizationResource
"""Deactivate authorization.
:param messages.AuthorizationResource authzr: The Authorization resource
@@ -191,7 +199,7 @@ class ClientBase:
when = parsedate_tz(retry_after)
if when is not None:
try:
tz_secs = datetime.timedelta(when[-1] if when[-1] is not None else 0)
tz_secs = datetime.timedelta(when[-1] if when[-1] else 0)
return datetime.datetime(*when[:7]) - tz_secs
except (ValueError, OverflowError):
pass
@@ -250,10 +258,10 @@ class Client(ClientBase):
if net is None:
net = ClientNetwork(key, alg=alg, verify_ssl=verify_ssl)
if isinstance(directory, str):
if isinstance(directory, six.string_types):
directory = messages.Directory.from_json(
net.get(directory).json())
super().__init__(directory=directory,
super(Client, self).__init__(directory=directory,
net=net, acme_version=1)
def register(self, new_reg=None):
@@ -426,7 +434,7 @@ class Client(ClientBase):
"""
assert max_attempts > 0
attempts: Dict[messages.AuthorizationResource, int] = collections.defaultdict(int)
attempts = collections.defaultdict(int) # type: Dict[messages.AuthorizationResource, int]
exhausted = set()
# priority queue with datetime.datetime (based on Retry-After) as key,
@@ -438,7 +446,7 @@ class Client(ClientBase):
heapq.heapify(waiting)
# mapping between original Authorization Resource and the most
# recently updated one
updated = {authzr: authzr for authzr in authzrs}
updated = dict((authzr, authzr) for authzr in authzrs)
while waiting:
# find the smallest Retry-After, and sleep if necessary
@@ -465,7 +473,7 @@ class Client(ClientBase):
exhausted.add(authzr)
if exhausted or any(authzr.body.status == messages.STATUS_INVALID
for authzr in updated.values()):
for authzr in six.itervalues(updated)):
raise errors.PollError(exhausted, updated)
updated_authzrs = tuple(updated[authzr] for authzr in authzrs)
@@ -539,7 +547,7 @@ class Client(ClientBase):
:rtype: `list` of `OpenSSL.crypto.X509` wrapped in `.ComparableX509`
"""
chain: List[jose.ComparableX509] = []
chain = [] # type: List[jose.ComparableX509]
uri = certr.cert_chain_uri
while uri is not None and len(chain) < max_length:
response, cert = self._get_cert(uri)
@@ -577,7 +585,7 @@ class ClientV2(ClientBase):
:param .messages.Directory directory: Directory Resource
:param .ClientNetwork net: Client network.
"""
super().__init__(directory=directory,
super(ClientV2, self).__init__(directory=directory,
net=net, acme_version=2)
def new_account(self, new_account):
@@ -627,7 +635,7 @@ class ClientV2(ClientBase):
"""
# https://github.com/certbot/certbot/issues/6155
new_regr = self._get_v2_account(regr)
return super().update_registration(new_regr, update)
return super(ClientV2, self).update_registration(new_regr, update)
def _get_v2_account(self, regr):
self.net.account = None
@@ -724,13 +732,11 @@ class ClientV2(ClientBase):
raise errors.ValidationError(failed)
return orderr.update(authorizations=responses)
def finalize_order(self, orderr, deadline, fetch_alternative_chains=False):
def finalize_order(self, orderr, deadline):
"""Finalize an order and obtain a certificate.
:param messages.OrderResource orderr: order to finalize
:param datetime.datetime deadline: when to stop polling and timeout
:param bool fetch_alternative_chains: whether to also fetch alternative
certificate chains
:returns: finalized order
:rtype: messages.OrderResource
@@ -747,13 +753,8 @@ class ClientV2(ClientBase):
if body.error is not None:
raise errors.IssuanceError(body.error)
if body.certificate is not None:
certificate_response = self._post_as_get(body.certificate)
orderr = orderr.update(body=body, fullchain_pem=certificate_response.text)
if fetch_alternative_chains:
alt_chains_urls = self._get_links(certificate_response, 'alternate')
alt_chains = [self._post_as_get(url).text for url in alt_chains_urls]
orderr = orderr.update(alternative_fullchains_pem=alt_chains)
return orderr
certificate_response = self._post_as_get(body.certificate).text
return orderr.update(body=body, fullchain_pem=certificate_response)
raise errors.TimeoutError()
def revoke(self, cert, rsn):
@@ -783,22 +784,8 @@ class ClientV2(ClientBase):
new_args = args[:1] + (None,) + args[1:]
return self._post(*new_args, **kwargs)
def _get_links(self, response, relation_type):
"""
Retrieves all Link URIs of relation_type from the response.
:param requests.Response response: The requests HTTP response.
:param str relation_type: The relation type to filter by.
"""
# Can't use response.links directly because it drops multiple links
# of the same relation type, which is possible in RFC8555 responses.
if 'Link' not in response.headers:
return []
links = parse_header_links(response.headers['Link'])
return [l['url'] for l in links
if 'rel' in l and 'url' in l and l['rel'] == relation_type]
class BackwardsCompatibleClientV2:
class BackwardsCompatibleClientV2(object):
"""ACME client wrapper that tends towards V2-style calls, but
supports V1 servers.
@@ -820,7 +807,6 @@ class BackwardsCompatibleClientV2:
def __init__(self, net, key, server):
directory = messages.Directory.from_json(net.get(server).json())
self.acme_version = self._acme_version_from_directory(directory)
self.client: Union[Client, ClientV2]
if self.acme_version == 1:
self.client = Client(directory, key=key, net=net)
else:
@@ -840,18 +826,16 @@ class BackwardsCompatibleClientV2:
if check_tos_cb is not None:
check_tos_cb(tos)
if self.acme_version == 1:
client_v1 = cast(Client, self.client)
regr = client_v1.register(regr)
regr = self.client.register(regr)
if regr.terms_of_service is not None:
_assess_tos(regr.terms_of_service)
return client_v1.agree_to_tos(regr)
return self.client.agree_to_tos(regr)
return regr
else:
client_v2 = cast(ClientV2, self.client)
if "terms_of_service" in client_v2.directory.meta:
_assess_tos(client_v2.directory.meta.terms_of_service)
if "terms_of_service" in self.client.directory.meta:
_assess_tos(self.client.directory.meta.terms_of_service)
regr = regr.update(terms_of_service_agreed=True)
return client_v2.new_account(regr)
return self.client.new_account(regr)
def new_order(self, csr_pem):
"""Request a new Order object from the server.
@@ -869,32 +853,28 @@ class BackwardsCompatibleClientV2:
"""
if self.acme_version == 1:
client_v1 = cast(Client, self.client)
csr = OpenSSL.crypto.load_certificate_request(OpenSSL.crypto.FILETYPE_PEM, csr_pem)
# pylint: disable=protected-access
dnsNames = crypto_util._pyopenssl_cert_or_req_all_names(csr)
authorizations = []
for domain in dnsNames:
authorizations.append(client_v1.request_domain_challenges(domain))
authorizations.append(self.client.request_domain_challenges(domain))
return messages.OrderResource(authorizations=authorizations, csr_pem=csr_pem)
return cast(ClientV2, self.client).new_order(csr_pem)
return self.client.new_order(csr_pem)
def finalize_order(self, orderr, deadline, fetch_alternative_chains=False):
def finalize_order(self, orderr, deadline):
"""Finalize an order and obtain a certificate.
:param messages.OrderResource orderr: order to finalize
:param datetime.datetime deadline: when to stop polling and timeout
:param bool fetch_alternative_chains: whether to also fetch alternative
certificate chains
:returns: finalized order
:rtype: messages.OrderResource
"""
if self.acme_version == 1:
client_v1 = cast(Client, self.client)
csr_pem = orderr.csr_pem
certr = client_v1.request_issuance(
certr = self.client.request_issuance(
jose.ComparableX509(
OpenSSL.crypto.load_certificate_request(OpenSSL.crypto.FILETYPE_PEM, csr_pem)),
orderr.authorizations)
@@ -902,7 +882,7 @@ class BackwardsCompatibleClientV2:
chain = None
while datetime.datetime.now() < deadline:
try:
chain = client_v1.fetch_chain(certr)
chain = self.client.fetch_chain(certr)
break
except errors.Error:
time.sleep(1)
@@ -917,8 +897,7 @@ class BackwardsCompatibleClientV2:
chain = crypto_util.dump_pyopenssl_chain(chain).decode()
return orderr.update(fullchain_pem=(cert + chain))
return cast(ClientV2, self.client).finalize_order(
orderr, deadline, fetch_alternative_chains)
return self.client.finalize_order(orderr, deadline)
def revoke(self, cert, rsn):
"""Revoke certificate.
@@ -944,10 +923,10 @@ class BackwardsCompatibleClientV2:
Always return False for ACMEv1 servers, as it doesn't use External Account Binding."""
if self.acme_version == 1:
return False
return cast(ClientV2, self.client).external_account_required()
return self.client.external_account_required()
class ClientNetwork:
class ClientNetwork(object):
"""Wrapper around requests that signs POSTs for authentication.
Also adds user agent, and handles Content-Type.
@@ -977,7 +956,7 @@ class ClientNetwork:
self.account = account
self.alg = alg
self.verify_ssl = verify_ssl
self._nonces: Set[Text] = set()
self._nonces = set() # type: Set[Text]
self.user_agent = user_agent
self.session = requests.Session()
self._default_timeout = timeout
@@ -1008,8 +987,6 @@ class ClientNetwork:
:rtype: `josepy.JWS`
"""
if isinstance(obj, VersionedLEACMEMixin):
obj.le_acme_version = acme_version
jobj = obj.json_dumps(indent=2).encode() if obj else b''
logger.debug('JWS payload:\n%s', jobj)
kwargs = {
@@ -1137,15 +1114,14 @@ class ClientNetwork:
# If content is DER, log the base64 of it instead of raw bytes, to keep
# binary data out of the logs.
debug_content: Union[bytes, str]
if response.headers.get("Content-Type") == DER_CONTENT_TYPE:
debug_content = base64.b64encode(response.content)
else:
debug_content = response.content.decode("utf-8")
logger.debug('Received response:\nHTTP %d\n%s\n\n%s',
response.status_code,
"\n".join("{0}: {1}".format(k, v)
for k, v in response.headers.items()),
"\n".join(["{0}: {1}".format(k, v)
for k, v in response.headers.items()]),
debug_content)
return response

View File

@@ -5,15 +5,15 @@ import logging
import os
import re
import socket
from typing import Callable
from typing import Tuple
from typing import Union
import josepy as jose
from OpenSSL import crypto
from OpenSSL import SSL # type: ignore # https://github.com/python/typeshed/issues/2052
from acme import errors
from acme.magic_typing import Callable
from acme.magic_typing import Tuple
from acme.magic_typing import Union
logger = logging.getLogger(__name__)
@@ -27,41 +27,19 @@ logger = logging.getLogger(__name__)
_DEFAULT_SSL_METHOD = SSL.SSLv23_METHOD # type: ignore
class _DefaultCertSelection:
def __init__(self, certs):
self.certs = certs
def __call__(self, connection):
server_name = connection.get_servername()
return self.certs.get(server_name, None)
class SSLSocket: # pylint: disable=too-few-public-methods
class SSLSocket(object):
"""SSL wrapper for sockets.
:ivar socket sock: Original wrapped socket.
:ivar dict certs: Mapping from domain names (`bytes`) to
`OpenSSL.crypto.X509`.
:ivar method: See `OpenSSL.SSL.Context` for allowed values.
:ivar alpn_selection: Hook to select negotiated ALPN protocol for
connection.
:ivar cert_selection: Hook to select certificate for connection. If given,
`certs` parameter would be ignored, and therefore must be empty.
"""
def __init__(self, sock, certs=None,
method=_DEFAULT_SSL_METHOD, alpn_selection=None,
cert_selection=None):
def __init__(self, sock, certs, method=_DEFAULT_SSL_METHOD):
self.sock = sock
self.alpn_selection = alpn_selection
self.certs = certs
self.method = method
if not cert_selection and not certs:
raise ValueError("Neither cert_selection or certs specified.")
if cert_selection and certs:
raise ValueError("Both cert_selection and certs specified.")
if cert_selection is None:
cert_selection = _DefaultCertSelection(certs)
self.cert_selection = cert_selection
def __getattr__(self, name):
return getattr(self.sock, name)
@@ -78,22 +56,21 @@ class SSLSocket: # pylint: disable=too-few-public-methods
:type connection: :class:`OpenSSL.Connection`
"""
pair = self.cert_selection(connection)
if pair is None:
logger.debug("Certificate selection for server name %s failed, dropping SSL",
connection.get_servername())
server_name = connection.get_servername()
try:
key, cert = self.certs[server_name]
except KeyError:
logger.debug("Server name (%s) not recognized, dropping SSL",
server_name)
return
key, cert = pair
new_context = SSL.Context(self.method)
new_context.set_options(SSL.OP_NO_SSLv2)
new_context.set_options(SSL.OP_NO_SSLv3)
new_context.use_privatekey(key)
new_context.use_certificate(cert)
if self.alpn_selection is not None:
new_context.set_alpn_select_callback(self.alpn_selection)
connection.set_context(new_context)
class FakeConnection:
class FakeConnection(object):
"""Fake OpenSSL.SSL.Connection."""
# pylint: disable=missing-function-docstring
@@ -115,8 +92,6 @@ class SSLSocket: # pylint: disable=too-few-public-methods
context.set_options(SSL.OP_NO_SSLv2)
context.set_options(SSL.OP_NO_SSLv3)
context.set_tlsext_servername_callback(self._pick_certificate_cb)
if self.alpn_selection is not None:
context.set_alpn_select_callback(self.alpn_selection)
ssl_sock = self.FakeConnection(SSL.Connection(context, sock))
ssl_sock.set_accept_state()
@@ -132,9 +107,8 @@ class SSLSocket: # pylint: disable=too-few-public-methods
return ssl_sock, addr
def probe_sni(name, host, port=443, timeout=300, # pylint: disable=too-many-arguments
method=_DEFAULT_SSL_METHOD, source_address=('', 0),
alpn_protocols=None):
def probe_sni(name, host, port=443, timeout=300,
method=_DEFAULT_SSL_METHOD, source_address=('', 0)):
"""Probe SNI server for SSL certificate.
:param bytes name: Byte string to send as the server name in the
@@ -146,8 +120,6 @@ def probe_sni(name, host, port=443, timeout=300, # pylint: disable=too-many-argu
:param tuple source_address: Enables multi-path probing (selection
of source interface). See `socket.creation_connection` for more
info. Available only in Python 2.7+.
:param alpn_protocols: Protocols to request using ALPN.
:type alpn_protocols: `list` of `bytes`
:raises acme.errors.Error: In case of any problems.
@@ -166,9 +138,9 @@ def probe_sni(name, host, port=443, timeout=300, # pylint: disable=too-many-argu
" from {0}:{1}".format(
source_address[0],
source_address[1]
) if any(source_address) else ""
) if socket_kwargs else ""
)
socket_tuple: Tuple[str, int] = (host, port)
socket_tuple = (host, port) # type: Tuple[str, int]
sock = socket.create_connection(socket_tuple, **socket_kwargs) # type: ignore
except socket.error as error:
raise errors.Error(error)
@@ -177,8 +149,6 @@ def probe_sni(name, host, port=443, timeout=300, # pylint: disable=too-many-argu
client_ssl = SSL.Connection(context, client)
client_ssl.set_connect_state()
client_ssl.set_tlsext_host_name(name) # pyOpenSSL>=0.13
if alpn_protocols is not None:
client_ssl.set_alpn_protos(alpn_protocols)
try:
client_ssl.do_handshake()
client_ssl.shutdown()
@@ -186,7 +156,6 @@ def probe_sni(name, host, port=443, timeout=300, # pylint: disable=too-many-argu
raise errors.Error(error)
return client_ssl.get_peer_certificate()
def make_csr(private_key_pem, domains, must_staple=False):
"""Generate a CSR containing a list of domains as subjectAltNames.
@@ -218,7 +187,6 @@ def make_csr(private_key_pem, domains, must_staple=False):
return crypto.dump_certificate_request(
crypto.FILETYPE_PEM, csr)
def _pyopenssl_cert_or_req_all_names(loaded_cert_or_req):
common_name = loaded_cert_or_req.get_subject().CN
sans = _pyopenssl_cert_or_req_san(loaded_cert_or_req)
@@ -227,7 +195,6 @@ def _pyopenssl_cert_or_req_all_names(loaded_cert_or_req):
return sans
return [common_name] + [d for d in sans if d != common_name]
def _pyopenssl_cert_or_req_san(cert_or_req):
"""Get Subject Alternative Names from certificate or CSR using pyOpenSSL.
@@ -256,7 +223,7 @@ def _pyopenssl_cert_or_req_san(cert_or_req):
if isinstance(cert_or_req, crypto.X509):
# pylint: disable=line-too-long
func: Union[Callable[[int, crypto.X509Req], bytes], Callable[[int, crypto.X509], bytes]] = crypto.dump_certificate
func = crypto.dump_certificate # type: Union[Callable[[int, crypto.X509Req], bytes], Callable[[int, crypto.X509], bytes]]
else:
func = crypto.dump_certificate_request
text = func(crypto.FILETYPE_TEXT, cert_or_req).decode("utf-8")
@@ -272,14 +239,12 @@ def _pyopenssl_cert_or_req_san(cert_or_req):
def gen_ss_cert(key, domains, not_before=None,
validity=(7 * 24 * 60 * 60), force_san=True, extensions=None):
validity=(7 * 24 * 60 * 60), force_san=True):
"""Generate new self-signed certificate.
:type domains: `list` of `unicode`
:param OpenSSL.crypto.PKey key:
:param bool force_san:
:param extensions: List of additional extensions to include in the cert.
:type extensions: `list` of `OpenSSL.crypto.X509Extension`
If more than one domain is provided, all of the domains are put into
``subjectAltName`` X.509 extension and first domain is set as the
@@ -292,13 +257,10 @@ def gen_ss_cert(key, domains, not_before=None,
cert.set_serial_number(int(binascii.hexlify(os.urandom(16)), 16))
cert.set_version(2)
if extensions is None:
extensions = []
extensions.append(
extensions = [
crypto.X509Extension(
b"basicConstraints", True, b"CA:TRUE, pathlen:0"),
)
]
cert.get_subject().CN = domains[0]
# TODO: what to put into cert.get_subject()?
@@ -320,7 +282,6 @@ def gen_ss_cert(key, domains, not_before=None,
cert.sign(key, "sha256")
return cert
def dump_pyopenssl_chain(chain, filetype=crypto.FILETYPE_PEM):
"""Dump certificate chain into a bundle.

View File

@@ -28,8 +28,13 @@ class NonceError(ClientError):
class BadNonce(NonceError):
"""Bad nonce error."""
def __init__(self, nonce, error, *args):
super().__init__(*args)
def __init__(self, nonce, error, *args, **kwargs):
# MyPy complains here that there is too many arguments for BaseException constructor.
# This is an error fixed in typeshed, see https://github.com/python/mypy/issues/4183
# The fix is included in MyPy>=0.740, but upgrading it would bring dozen of errors due to
# new types definitions. So we ignore the error until the code base is fixed to match
# with MyPy>=0.740 referential.
super(BadNonce, self).__init__(*args, **kwargs) # type: ignore
self.nonce = nonce
self.error = error
@@ -44,11 +49,12 @@ class MissingNonce(NonceError):
Replay-Nonce header field in each successful response to a POST it
provides to a client (...)".
:ivar requests.Response ~.response: HTTP Response
:ivar requests.Response response: HTTP Response
"""
def __init__(self, response, *args):
super().__init__(*args)
def __init__(self, response, *args, **kwargs):
# See comment in BadNonce constructor above for an explanation of type: ignore here.
super(MissingNonce, self).__init__(*args, **kwargs) # type: ignore
self.response = response
def __str__(self):
@@ -72,7 +78,7 @@ class PollError(ClientError):
def __init__(self, exhausted, updated):
self.exhausted = exhausted
self.updated = updated
super().__init__()
super(PollError, self).__init__()
@property
def timeout(self):
@@ -90,7 +96,7 @@ class ValidationError(Error):
"""
def __init__(self, failed_authzrs):
self.failed_authzrs = failed_authzrs
super().__init__()
super(ValidationError, self).__init__()
class TimeoutError(Error): # pylint: disable=redefined-builtin
@@ -106,7 +112,7 @@ class IssuanceError(Error):
:param messages.Error error: The error provided by the server.
"""
self.error = error
super().__init__()
super(IssuanceError, self).__init__()
class ConflictError(ClientError):
@@ -119,7 +125,7 @@ class ConflictError(ClientError):
"""
def __init__(self, location):
self.location = location
super().__init__()
super(ConflictError, self).__init__()
class WildcardUnsupportedError(Error):

View File

@@ -12,7 +12,7 @@ class Fixed(jose.Field):
def __init__(self, json_name, value):
self.value = value
super().__init__(
super(Fixed, self).__init__(
json_name=json_name, default=value, omitempty=False)
def decode(self, value):
@@ -53,7 +53,7 @@ class Resource(jose.Field):
def __init__(self, resource_type, *args, **kwargs):
self.resource_type = resource_type
super().__init__(
super(Resource, self).__init__(
'resource', default=resource_type, *args, **kwargs)
def decode(self, value):

View File

@@ -14,9 +14,7 @@ class Header(jose.Header):
kid = jose.Field('kid', omitempty=True)
url = jose.Field('url', omitempty=True)
# Mypy does not understand the josepy magic happening here, and falsely claims
# that nonce is redefined. Let's ignore the type check here.
@nonce.decoder # type: ignore
@nonce.decoder
def nonce(value): # pylint: disable=no-self-argument,missing-function-docstring
try:
return jose.decode_b64jose(value)
@@ -50,7 +48,7 @@ class JWS(jose.JWS):
# Per ACME spec, jwk and kid are mutually exclusive, so only include a
# jwk field if kid is not provided.
include_jwk = kid is None
return super().sign(payload, key=key, alg=alg,
return super(JWS, cls).sign(payload, key=key, alg=alg,
protect=frozenset(['nonce', 'url', 'kid', 'jwk', 'alg']),
nonce=nonce, url=url, kid=kid,
include_jwk=include_jwk)

View File

@@ -1,17 +1,16 @@
"""Simple shim around the typing module.
"""Shim class to not have to depend on typing module in prod."""
import sys
This was useful when this code supported Python 2 and typing wasn't always
available. This code is being kept for now for backwards compatibility.
"""
import warnings
from typing import * # pylint: disable=wildcard-import, unused-wildcard-import
from typing import Collection, IO # type: ignore
warnings.warn("acme.magic_typing is deprecated and will be removed in a future release.",
DeprecationWarning)
class TypingClass:
class TypingClass(object):
"""Ignore import errors by getting anything"""
def __getattr__(self, name):
return None # pragma: no cover
return None
try:
# mypy doesn't respect modifying sys.modules
from typing import * # pylint: disable=wildcard-import, unused-wildcard-import
from typing import Collection, IO # type: ignore
# pylint: enable=unused-import
except ImportError:
sys.modules[__name__] = TypingClass()

View File

@@ -1,18 +1,21 @@
"""ACME protocol messages."""
from collections.abc import Hashable
import json
from typing import Any
from typing import Dict
from typing import Type
import josepy as jose
import six
from acme import challenges
from acme import errors
from acme import fields
from acme import jws
from acme import util
from acme.mixins import ResourceMixin
try:
from collections.abc import Hashable
except ImportError: # pragma: no cover
from collections import Hashable
OLD_ERROR_PREFIX = "urn:acme:error:"
ERROR_PREFIX = "urn:ietf:params:acme:error:"
@@ -64,6 +67,7 @@ def is_acme_error(err):
return False
@six.python_2_unicode_compatible
class Error(jose.JSONObjectWithFields, errors.Error):
"""ACME error.
@@ -90,9 +94,7 @@ class Error(jose.JSONObjectWithFields, errors.Error):
raise ValueError("The supplied code: %s is not a known ACME error"
" code" % code)
typ = ERROR_PREFIX + code
# Mypy will not understand that the Error constructor accepts a named argument
# "typ" because of josepy magic. Let's ignore the type check here.
return cls(typ=typ, **kwargs) # type: ignore
return cls(typ=typ, **kwargs)
@property
def description(self):
@@ -129,10 +131,10 @@ class Error(jose.JSONObjectWithFields, errors.Error):
class _Constant(jose.JSONDeSerializable, Hashable): # type: ignore
"""ACME constant."""
__slots__ = ('name',)
POSSIBLE_NAMES: Dict[str, '_Constant'] = NotImplemented
POSSIBLE_NAMES = NotImplemented
def __init__(self, name):
super().__init__()
super(_Constant, self).__init__()
self.POSSIBLE_NAMES[name] = self # pylint: disable=unsupported-assignment-operation
self.name = name
@@ -155,10 +157,13 @@ class _Constant(jose.JSONDeSerializable, Hashable): # type: ignore
def __hash__(self):
return hash((self.__class__, self.name))
def __ne__(self, other):
return not self == other
class Status(_Constant):
"""ACME "status" field."""
POSSIBLE_NAMES: dict = {}
POSSIBLE_NAMES = {} # type: dict
STATUS_UNKNOWN = Status('unknown')
STATUS_PENDING = Status('pending')
STATUS_PROCESSING = Status('processing')
@@ -171,7 +176,7 @@ STATUS_DEACTIVATED = Status('deactivated')
class IdentifierType(_Constant):
"""ACME identifier type."""
POSSIBLE_NAMES: Dict[str, 'IdentifierType'] = {}
POSSIBLE_NAMES = {} # type: dict
IDENTIFIER_FQDN = IdentifierType('dns') # IdentifierDNS in Boulder
@@ -189,7 +194,7 @@ class Identifier(jose.JSONObjectWithFields):
class Directory(jose.JSONDeSerializable):
"""Directory."""
_REGISTERED_TYPES: Dict[str, Type[Any]] = {}
_REGISTERED_TYPES = {} # type: dict
class Meta(jose.JSONObjectWithFields):
"""Directory Meta."""
@@ -200,8 +205,8 @@ class Directory(jose.JSONDeSerializable):
external_account_required = jose.Field('externalAccountRequired', omitempty=True)
def __init__(self, **kwargs):
kwargs = {self._internal_name(k): v for k, v in kwargs.items()}
super().__init__(**kwargs)
kwargs = dict((self._internal_name(k), v) for k, v in kwargs.items())
super(Directory.Meta, self).__init__(**kwargs)
@property
def terms_of_service(self):
@@ -211,7 +216,7 @@ class Directory(jose.JSONDeSerializable):
def __iter__(self):
# When iterating over fields, use the external name 'terms_of_service' instead of
# the internal '_terms_of_service'.
for name in super().__iter__():
for name in super(Directory.Meta, self).__iter__():
yield name[1:] if name == '_terms_of_service' else name
def _internal_name(self, name):
@@ -223,7 +228,7 @@ class Directory(jose.JSONDeSerializable):
return getattr(key, 'resource_type', key)
@classmethod
def register(cls, resource_body_cls: Type[Any]) -> Type[Any]:
def register(cls, resource_body_cls):
"""Register resource."""
resource_type = resource_body_cls.resource_type
assert resource_type not in cls._REGISTERED_TYPES
@@ -269,7 +274,7 @@ class Resource(jose.JSONObjectWithFields):
class ResourceWithURI(Resource):
"""ACME Resource with URI.
:ivar unicode ~.uri: Location of the resource.
:ivar unicode uri: Location of the resource.
"""
uri = jose.Field('uri') # no ChallengeResource.uri
@@ -279,7 +284,7 @@ class ResourceBody(jose.JSONObjectWithFields):
"""ACME Resource Body."""
class ExternalAccountBinding:
class ExternalAccountBinding(object):
"""ACME External Account Binding"""
@classmethod
@@ -309,9 +314,6 @@ class Registration(ResourceBody):
# on new-reg key server ignores 'key' and populates it based on
# JWS.signature.combined.jwk
key = jose.Field('key', omitempty=True, decoder=jose.JWK.from_json)
# Contact field implements special behavior to allow messages that clear existing
# contacts while not expecting the `contact` field when loading from json.
# This is implemented in the constructor and *_json methods.
contact = jose.Field('contact', omitempty=True, default=())
agreement = jose.Field('agreement', omitempty=True)
status = jose.Field('status', omitempty=True)
@@ -324,73 +326,24 @@ class Registration(ResourceBody):
@classmethod
def from_data(cls, phone=None, email=None, external_account_binding=None, **kwargs):
"""
Create registration resource from contact details.
The `contact` keyword being passed to a Registration object is meaningful, so
this function represents empty iterables in its kwargs by passing on an empty
`tuple`.
"""
# Note if `contact` was in kwargs.
contact_provided = 'contact' in kwargs
# Pop `contact` from kwargs and add formatted email or phone numbers
"""Create registration resource from contact details."""
details = list(kwargs.pop('contact', ()))
if phone is not None:
details.append(cls.phone_prefix + phone)
if email is not None:
details.extend([cls.email_prefix + mail for mail in email.split(',')])
# Insert formatted contact information back into kwargs
# or insert an empty tuple if `contact` provided.
if details or contact_provided:
kwargs['contact'] = tuple(details)
kwargs['contact'] = tuple(details)
if external_account_binding:
kwargs['external_account_binding'] = external_account_binding
return cls(**kwargs)
def __init__(self, **kwargs):
"""Note if the user provides a value for the `contact` member."""
if 'contact' in kwargs:
# Avoid the __setattr__ used by jose.TypedJSONObjectWithFields
object.__setattr__(self, '_add_contact', True)
super().__init__(**kwargs)
def _filter_contact(self, prefix):
return tuple(
detail[len(prefix):] for detail in self.contact # pylint: disable=not-an-iterable
if detail.startswith(prefix))
def _add_contact_if_appropriate(self, jobj):
"""
The `contact` member of Registration objects should not be required when
de-serializing (as it would be if the Fields' `omitempty` flag were `False`), but
it should be included in serializations if it was provided.
:param jobj: Dictionary containing this Registrations' data
:type jobj: dict
:returns: Dictionary containing Registrations data to transmit to the server
:rtype: dict
"""
if getattr(self, '_add_contact', False):
jobj['contact'] = self.encode('contact')
return jobj
def to_partial_json(self):
"""Modify josepy.JSONDeserializable.to_partial_json()"""
jobj = super().to_partial_json()
return self._add_contact_if_appropriate(jobj)
def fields_to_partial_json(self):
"""Modify josepy.JSONObjectWithFields.fields_to_partial_json()"""
jobj = super().fields_to_partial_json()
return self._add_contact_if_appropriate(jobj)
@property
def phones(self):
"""All phones found in the ``contact`` field."""
@@ -403,13 +356,13 @@ class Registration(ResourceBody):
@Directory.register
class NewRegistration(ResourceMixin, Registration):
class NewRegistration(Registration):
"""New registration."""
resource_type = 'new-reg'
resource = fields.Resource(resource_type)
class UpdateRegistration(ResourceMixin, Registration):
class UpdateRegistration(Registration):
"""Update registration."""
resource_type = 'reg'
resource = fields.Resource(resource_type)
@@ -459,20 +412,20 @@ class ChallengeBody(ResourceBody):
omitempty=True, default=None)
def __init__(self, **kwargs):
kwargs = {self._internal_name(k): v for k, v in kwargs.items()}
super().__init__(**kwargs)
kwargs = dict((self._internal_name(k), v) for k, v in kwargs.items())
super(ChallengeBody, self).__init__(**kwargs)
def encode(self, name):
return super().encode(self._internal_name(name))
return super(ChallengeBody, self).encode(self._internal_name(name))
def to_partial_json(self):
jobj = super().to_partial_json()
jobj = super(ChallengeBody, self).to_partial_json()
jobj.update(self.chall.to_partial_json())
return jobj
@classmethod
def fields_from_json(cls, jobj):
jobj_fields = super().fields_from_json(jobj)
jobj_fields = super(ChallengeBody, cls).fields_from_json(jobj)
jobj_fields['chall'] = challenges.Challenge.from_json(jobj)
return jobj_fields
@@ -487,7 +440,7 @@ class ChallengeBody(ResourceBody):
def __iter__(self):
# When iterating over fields, use the external name 'uri' instead of
# the internal '_uri'.
for name in super().__iter__():
for name in super(ChallengeBody, self).__iter__():
yield name[1:] if name == '_uri' else name
def _internal_name(self, name):
@@ -533,9 +486,7 @@ class Authorization(ResourceBody):
expires = fields.RFC3339Field('expires', omitempty=True)
wildcard = jose.Field('wildcard', omitempty=True)
# Mypy does not understand the josepy magic happening here, and falsely claims
# that challenge is redefined. Let's ignore the type check here.
@challenges.decoder # type: ignore
@challenges.decoder
def challenges(value): # pylint: disable=no-self-argument,missing-function-docstring
return tuple(ChallengeBody.from_json(chall) for chall in value)
@@ -547,13 +498,13 @@ class Authorization(ResourceBody):
@Directory.register
class NewAuthorization(ResourceMixin, Authorization):
class NewAuthorization(Authorization):
"""New authorization."""
resource_type = 'new-authz'
resource = fields.Resource(resource_type)
class UpdateAuthorization(ResourceMixin, Authorization):
class UpdateAuthorization(Authorization):
"""Update authorization."""
resource_type = 'authz'
resource = fields.Resource(resource_type)
@@ -571,7 +522,7 @@ class AuthorizationResource(ResourceWithURI):
@Directory.register
class CertificateRequest(ResourceMixin, jose.JSONObjectWithFields):
class CertificateRequest(jose.JSONObjectWithFields):
"""ACME new-cert request.
:ivar josepy.util.ComparableX509 csr:
@@ -597,7 +548,7 @@ class CertificateResource(ResourceWithURI):
@Directory.register
class Revocation(ResourceMixin, jose.JSONObjectWithFields):
class Revocation(jose.JSONObjectWithFields):
"""Revocation message.
:ivar .ComparableX509 certificate: `OpenSSL.crypto.X509` wrapped in
@@ -614,16 +565,14 @@ class Revocation(ResourceMixin, jose.JSONObjectWithFields):
class Order(ResourceBody):
"""Order Resource Body.
:ivar identifiers: List of identifiers for the certificate.
:vartype identifiers: `list` of `.Identifier`
:ivar list of .Identifier: List of identifiers for the certificate.
:ivar acme.messages.Status status:
:ivar authorizations: URLs of authorizations.
:vartype authorizations: `list` of `str`
:ivar list of str authorizations: URLs of authorizations.
:ivar str certificate: URL to download certificate as a fullchain PEM.
:ivar str finalize: URL to POST to to request issuance once all
authorizations have "valid" status.
:ivar datetime.datetime expires: When the order expires.
:ivar ~.Error error: Any error that occurred during finalization, if applicable.
:ivar .Error error: Any error that occurred during finalization, if applicable.
"""
identifiers = jose.Field('identifiers', omitempty=True)
status = jose.Field('status', decoder=Status.from_json,
@@ -634,9 +583,7 @@ class Order(ResourceBody):
expires = fields.RFC3339Field('expires', omitempty=True)
error = jose.Field('error', omitempty=True, decoder=Error.from_json)
# Mypy does not understand the josepy magic happening here, and falsely claims
# that identifiers is redefined. Let's ignore the type check here.
@identifiers.decoder # type: ignore
@identifiers.decoder
def identifiers(value): # pylint: disable=no-self-argument,missing-function-docstring
return tuple(Identifier.from_json(identifier) for identifier in value)
@@ -645,20 +592,15 @@ class OrderResource(ResourceWithURI):
:ivar acme.messages.Order body:
:ivar str csr_pem: The CSR this Order will be finalized with.
:ivar authorizations: Fully-fetched AuthorizationResource objects.
:vartype authorizations: `list` of `acme.messages.AuthorizationResource`
:ivar list of acme.messages.AuthorizationResource authorizations:
Fully-fetched AuthorizationResource objects.
:ivar str fullchain_pem: The fetched contents of the certificate URL
produced once the order was finalized, if it's present.
:ivar alternative_fullchains_pem: The fetched contents of alternative certificate
chain URLs produced once the order was finalized, if present and requested during
finalization.
:vartype alternative_fullchains_pem: `list` of `str`
"""
body = jose.Field('body', decoder=Order.from_json)
csr_pem = jose.Field('csr_pem', omitempty=True)
authorizations = jose.Field('authorizations')
fullchain_pem = jose.Field('fullchain_pem', omitempty=True)
alternative_fullchains_pem = jose.Field('alternative_fullchains_pem', omitempty=True)
@Directory.register
class NewOrder(Order):

View File

@@ -1,65 +0,0 @@
"""Useful mixins for Challenge and Resource objects"""
class VersionedLEACMEMixin:
"""This mixin stores the version of Let's Encrypt's endpoint being used."""
@property
def le_acme_version(self):
"""Define the version of ACME protocol to use"""
return getattr(self, '_le_acme_version', 1)
@le_acme_version.setter
def le_acme_version(self, version):
# We need to use object.__setattr__ to not depend on the specific implementation of
# __setattr__ in current class (eg. jose.TypedJSONObjectWithFields raises AttributeError
# for any attempt to set an attribute to make objects immutable).
object.__setattr__(self, '_le_acme_version', version)
def __setattr__(self, key, value):
if key == 'le_acme_version':
# Required for @property to operate properly. See comment above.
object.__setattr__(self, key, value)
else:
super().__setattr__(key, value) # pragma: no cover
class ResourceMixin(VersionedLEACMEMixin):
"""
This mixin generates a RFC8555 compliant JWS payload
by removing the `resource` field if needed (eg. ACME v2 protocol).
"""
def to_partial_json(self):
"""See josepy.JSONDeserializable.to_partial_json()"""
return _safe_jobj_compliance(super(),
'to_partial_json', 'resource')
def fields_to_partial_json(self):
"""See josepy.JSONObjectWithFields.fields_to_partial_json()"""
return _safe_jobj_compliance(super(),
'fields_to_partial_json', 'resource')
class TypeMixin(VersionedLEACMEMixin):
"""
This mixin allows generation of a RFC8555 compliant JWS payload
by removing the `type` field if needed (eg. ACME v2 protocol).
"""
def to_partial_json(self):
"""See josepy.JSONDeserializable.to_partial_json()"""
return _safe_jobj_compliance(super(),
'to_partial_json', 'type')
def fields_to_partial_json(self):
"""See josepy.JSONObjectWithFields.fields_to_partial_json()"""
return _safe_jobj_compliance(super(),
'fields_to_partial_json', 'type')
def _safe_jobj_compliance(instance, jobj_method, uncompliant_field):
if hasattr(instance, jobj_method):
jobj = getattr(instance, jobj_method)()
if instance.le_acme_version == 2:
jobj.pop(uncompliant_field, None)
return jobj
raise AttributeError('Method {0}() is not implemented.'.format(jobj_method)) # pragma: no cover

View File

@@ -1,16 +1,17 @@
"""Support for standalone client challenge solvers. """
import collections
import functools
import http.client as http_client
import http.server as BaseHTTPServer
import logging
import socket
import socketserver
import threading
from typing import List
from six.moves import BaseHTTPServer # type: ignore
from six.moves import http_client
from six.moves import socketserver # type: ignore
from acme import challenges
from acme import crypto_util
from acme.magic_typing import List
logger = logging.getLogger(__name__)
@@ -32,14 +33,7 @@ class TLSServer(socketserver.TCPServer):
def _wrap_sock(self):
self.socket = crypto_util.SSLSocket(
self.socket, cert_selection=self._cert_selection,
alpn_selection=getattr(self, '_alpn_selection', None),
method=self.method)
def _cert_selection(self, connection): # pragma: no cover
"""Callback selecting certificate for connection."""
server_name = connection.get_servername()
return self.certs.get(server_name, None)
self.socket, certs=self.certs, method=self.method)
def server_bind(self):
self._wrap_sock()
@@ -53,7 +47,7 @@ class ACMEServerMixin:
allow_reuse_address = True
class BaseDualNetworkedServers:
class BaseDualNetworkedServers(object):
"""Base class for a pair of IPv6 and IPv4 servers that tries to do everything
it's asked for both servers, but where failures in one server don't
affect the other.
@@ -63,8 +57,8 @@ class BaseDualNetworkedServers:
def __init__(self, ServerClass, server_address, *remaining_args, **kwargs):
port = server_address[1]
self.threads: List[threading.Thread] = []
self.servers: List[socketserver.BaseServer] = []
self.threads = [] # type: List[threading.Thread]
self.servers = [] # type: List[ACMEServerMixin]
# Must try True first.
# Ubuntu, for example, will fail to bind to IPv4 if we've already bound
@@ -126,40 +120,6 @@ class BaseDualNetworkedServers:
self.threads = []
class TLSALPN01Server(TLSServer, ACMEServerMixin):
"""TLSALPN01 Server."""
ACME_TLS_1_PROTOCOL = b"acme-tls/1"
def __init__(self, server_address, certs, challenge_certs, ipv6=False):
TLSServer.__init__(
self, server_address, _BaseRequestHandlerWithLogging, certs=certs,
ipv6=ipv6)
self.challenge_certs = challenge_certs
def _cert_selection(self, connection):
# TODO: We would like to serve challenge cert only if asked for it via
# ALPN. To do this, we need to retrieve the list of protos from client
# hello, but this is currently impossible with openssl [0], and ALPN
# negotiation is done after cert selection.
# Therefore, currently we always return challenge cert, and terminate
# handshake in alpn_selection() if ALPN protos are not what we expect.
# [0] https://github.com/openssl/openssl/issues/4952
server_name = connection.get_servername()
logger.debug("Serving challenge cert for server name %s", server_name)
return self.challenge_certs.get(server_name, None)
def _alpn_selection(self, _connection, alpn_protos):
"""Callback to select alpn protocol."""
if len(alpn_protos) == 1 and alpn_protos[0] == self.ACME_TLS_1_PROTOCOL:
logger.debug("Agreed on %s ALPN", self.ACME_TLS_1_PROTOCOL)
return self.ACME_TLS_1_PROTOCOL
logger.debug("Cannot agree on ALPN proto. Got: %s", str(alpn_protos))
# Explicitly close the connection now, by returning an empty string.
# See https://www.pyopenssl.org/en/stable/api/ssl.html#OpenSSL.SSL.Context.set_alpn_select_callback # pylint: disable=line-too-long
return b""
class HTTPServer(BaseHTTPServer.HTTPServer):
"""Generic HTTP Server."""
@@ -175,10 +135,10 @@ class HTTPServer(BaseHTTPServer.HTTPServer):
class HTTP01Server(HTTPServer, ACMEServerMixin):
"""HTTP01 Server."""
def __init__(self, server_address, resources, ipv6=False, timeout=30):
def __init__(self, server_address, resources, ipv6=False):
HTTPServer.__init__(
self, server_address, HTTP01RequestHandler.partial_init(
simple_http_resources=resources, timeout=timeout), ipv6=ipv6)
simple_http_resources=resources), ipv6=ipv6)
class HTTP01DualNetworkedServers(BaseDualNetworkedServers):
@@ -203,24 +163,7 @@ class HTTP01RequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
def __init__(self, *args, **kwargs):
self.simple_http_resources = kwargs.pop("simple_http_resources", set())
self._timeout = kwargs.pop('timeout', 30)
BaseHTTPServer.BaseHTTPRequestHandler.__init__(self, *args, **kwargs)
self.server: HTTP01Server
# In parent class BaseHTTPRequestHandler, 'timeout' is a class-level property but we
# need to define its value during the initialization phase in HTTP01RequestHandler.
# However MyPy does not appreciate that we dynamically shadow a class-level property
# with an instance-level property (eg. self.timeout = ... in __init__()). So to make
# everyone happy, we statically redefine 'timeout' as a method property, and set the
# timeout value in a new internal instance-level property _timeout.
@property
def timeout(self):
"""
The default timeout this server should apply to requests.
:return: timeout to apply
:rtype: int
"""
return self._timeout
def log_message(self, format, *args): # pylint: disable=redefined-builtin
"""Log arbitrary message."""
@@ -269,7 +212,7 @@ class HTTP01RequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
self.path)
@classmethod
def partial_init(cls, simple_http_resources, timeout):
def partial_init(cls, simple_http_resources):
"""Partially initialize this handler.
This is useful because `socketserver.BaseServer` takes
@@ -278,18 +221,4 @@ class HTTP01RequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
"""
return functools.partial(
cls, simple_http_resources=simple_http_resources,
timeout=timeout)
class _BaseRequestHandlerWithLogging(socketserver.BaseRequestHandler):
"""BaseRequestHandler with logging."""
def log_message(self, format, *args): # pylint: disable=redefined-builtin
"""Log arbitrary message."""
logger.debug("%s - - %s", self.client_address[0], format % args)
def handle(self):
"""Handle request."""
self.log_message("Incoming request")
socketserver.BaseRequestHandler.handle(self)
cls, simple_http_resources=simple_http_resources)

View File

@@ -1,6 +1,7 @@
"""ACME utilities."""
import six
def map_keys(dikt, func):
"""Map dictionary keys."""
return {func(key): value for key, value in dikt.items()}
return dict((func(key), value) for key, value in six.iteritems(dikt))

View File

@@ -9,7 +9,7 @@ BUILDDIR = _build
# User-friendly check for sphinx-build
ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1)
$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from https://www.sphinx-doc.org/)
$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/)
endif
# Internal variables.

View File

@@ -13,6 +13,7 @@
# serve to show the default.
import os
import shlex
import sys
here = os.path.abspath(os.path.dirname(__file__))
@@ -85,9 +86,7 @@ language = 'en'
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
exclude_patterns = [
'_build',
]
exclude_patterns = ['_build']
# The reST default role (used for this markup: `text`) to use for all
# documents.
@@ -122,7 +121,7 @@ todo_include_todos = False
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
# https://docs.readthedocs.io/en/stable/faq.html#i-want-to-use-the-read-the-docs-theme-locally
# http://docs.readthedocs.org/en/latest/theme.html#how-do-i-use-this-locally-and-on-read-the-docs
# on_rtd is whether we are on readthedocs.org
on_rtd = os.environ.get('READTHEDOCS', None) == 'True'
if not on_rtd: # only import and set the theme if we're building docs locally

View File

@@ -65,7 +65,7 @@ if errorlevel 9009 (
echo.may add the Sphinx directory to PATH.
echo.
echo.If you don't have Sphinx installed, grab it from
echo.https://www.sphinx-doc.org/
echo.http://sphinx-doc.org/
exit /b 1
)

View File

@@ -1,3 +1 @@
:orphan:
.. literalinclude:: ../jws-help.txt

View File

@@ -2,22 +2,28 @@ import sys
from setuptools import find_packages
from setuptools import setup
from setuptools.command.test import test as TestCommand
version = '1.16.0.dev0'
version = '1.3.0.dev0'
# Please update tox.ini when modifying dependency version requirements
install_requires = [
'cryptography>=2.1.4',
# load_pem_private/public_key (>=0.6)
# rsa_recover_prime_factors (>=0.8)
'cryptography>=1.2.3',
# formerly known as acme.jose:
# 1.1.0+ is required to avoid the warnings described at
# https://github.com/certbot/josepy/issues/13.
'josepy>=1.1.0',
'PyOpenSSL>=17.3.0',
'mock',
# Connection.set_tlsext_host_name (>=0.13)
'PyOpenSSL>=0.13.1',
'pyrfc3339',
'pytz',
'requests>=2.6.0',
'requests[security]>=2.6.0', # security extras added in 2.4.1
'requests-toolbelt>=0.3.0',
'setuptools>=39.0.1',
'setuptools',
'six>=1.9.0', # needed for python_2_unicode_compatible
]
dev_extras = [
@@ -31,25 +37,43 @@ docs_extras = [
'sphinx_rtd_theme',
]
class PyTest(TestCommand):
user_options = []
def initialize_options(self):
TestCommand.initialize_options(self)
self.pytest_args = ''
def run_tests(self):
import shlex
# import here, cause outside the eggs aren't loaded
import pytest
errno = pytest.main(shlex.split(self.pytest_args))
sys.exit(errno)
setup(
name='acme',
version=version,
description='ACME protocol implementation in Python',
url='https://github.com/letsencrypt/letsencrypt',
author="Certbot Project",
author_email='certbot-dev@eff.org',
author_email='client-dev@letsencrypt.org',
license='Apache License 2.0',
python_requires='>=3.6',
python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*',
classifiers=[
'Development Status :: 5 - Production/Stable',
'Intended Audience :: Developers',
'License :: OSI Approved :: Apache Software License',
'Programming Language :: Python',
'Programming Language :: Python :: 2',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.8',
'Programming Language :: Python :: 3.9',
'Topic :: Internet :: WWW/HTTP',
'Topic :: Security',
],
@@ -61,4 +85,7 @@ setup(
'dev': dev_extras,
'docs': docs_extras,
},
test_suite='acme',
tests_require=["pytest"],
cmdclass={"test": PyTest},
)

View File

@@ -1,13 +1,10 @@
"""Tests for acme.challenges."""
import urllib.parse as urllib_parse
import unittest
from unittest import mock
import josepy as jose
import OpenSSL
import mock
import requests
from acme import errors
from six.moves.urllib import parse as urllib_parse
import test_util
@@ -259,87 +256,30 @@ class HTTP01Test(unittest.TestCase):
class TLSALPN01ResponseTest(unittest.TestCase):
def setUp(self):
from acme.challenges import TLSALPN01
self.chall = TLSALPN01(
token=jose.b64decode(b'a82d5ff8ef740d12881f6d3c2277ab2e'))
self.domain = u'example.com'
self.domain2 = u'example2.com'
self.response = self.chall.response(KEY)
from acme.challenges import TLSALPN01Response
self.msg = TLSALPN01Response(key_authorization=u'foo')
self.jmsg = {
'resource': 'challenge',
'type': 'tls-alpn-01',
'keyAuthorization': self.response.key_authorization,
'keyAuthorization': u'foo',
}
from acme.challenges import TLSALPN01
self.chall = TLSALPN01(token=(b'x' * 16))
self.response = self.chall.response(KEY)
def test_to_partial_json(self):
self.assertEqual({k: v for k, v in self.jmsg.items() if k != 'keyAuthorization'},
self.response.to_partial_json())
self.msg.to_partial_json())
def test_from_json(self):
from acme.challenges import TLSALPN01Response
self.assertEqual(self.response, TLSALPN01Response.from_json(self.jmsg))
self.assertEqual(self.msg, TLSALPN01Response.from_json(self.jmsg))
def test_from_json_hashable(self):
from acme.challenges import TLSALPN01Response
hash(TLSALPN01Response.from_json(self.jmsg))
def test_gen_verify_cert(self):
key1 = test_util.load_pyopenssl_private_key('rsa512_key.pem')
cert, key2 = self.response.gen_cert(self.domain, key1)
self.assertEqual(key1, key2)
self.assertTrue(self.response.verify_cert(self.domain, cert))
def test_gen_verify_cert_gen_key(self):
cert, key = self.response.gen_cert(self.domain)
self.assertIsInstance(key, OpenSSL.crypto.PKey)
self.assertTrue(self.response.verify_cert(self.domain, cert))
def test_verify_bad_cert(self):
self.assertFalse(self.response.verify_cert(self.domain,
test_util.load_cert('cert.pem')))
def test_verify_bad_domain(self):
key1 = test_util.load_pyopenssl_private_key('rsa512_key.pem')
cert, key2 = self.response.gen_cert(self.domain, key1)
self.assertEqual(key1, key2)
self.assertFalse(self.response.verify_cert(self.domain2, cert))
def test_simple_verify_bad_key_authorization(self):
key2 = jose.JWKRSA.load(test_util.load_vector('rsa256_key.pem'))
self.response.simple_verify(self.chall, "local", key2.public_key())
@mock.patch('acme.challenges.TLSALPN01Response.verify_cert', autospec=True)
def test_simple_verify(self, mock_verify_cert):
mock_verify_cert.return_value = mock.sentinel.verification
self.assertEqual(
mock.sentinel.verification, self.response.simple_verify(
self.chall, self.domain, KEY.public_key(),
cert=mock.sentinel.cert))
mock_verify_cert.assert_called_once_with(
self.response, self.domain, mock.sentinel.cert)
@mock.patch('acme.challenges.socket.gethostbyname')
@mock.patch('acme.challenges.crypto_util.probe_sni')
def test_probe_cert(self, mock_probe_sni, mock_gethostbyname):
mock_gethostbyname.return_value = '127.0.0.1'
self.response.probe_cert('foo.com')
mock_gethostbyname.assert_called_once_with('foo.com')
mock_probe_sni.assert_called_once_with(
host='127.0.0.1', port=self.response.PORT, name='foo.com',
alpn_protocols=['acme-tls/1'])
self.response.probe_cert('foo.com', host='8.8.8.8')
mock_probe_sni.assert_called_with(
host='8.8.8.8', port=mock.ANY, name='foo.com',
alpn_protocols=['acme-tls/1'])
@mock.patch('acme.challenges.TLSALPN01Response.probe_cert')
def test_simple_verify_false_on_probe_error(self, mock_probe_cert):
mock_probe_cert.side_effect = errors.Error
self.assertFalse(self.response.simple_verify(
self.chall, self.domain, KEY.public_key()))
class TLSALPN01Test(unittest.TestCase):
@@ -369,13 +309,8 @@ class TLSALPN01Test(unittest.TestCase):
self.assertRaises(
jose.DeserializationError, TLSALPN01.from_json, self.jmsg)
@mock.patch('acme.challenges.TLSALPN01Response.gen_cert')
def test_validation(self, mock_gen_cert):
mock_gen_cert.return_value = ('cert', 'key')
self.assertEqual(('cert', 'key'), self.msg.validation(
KEY, cert_key=mock.sentinel.cert_key, domain=mock.sentinel.domain))
mock_gen_cert.assert_called_once_with(key=mock.sentinel.cert_key,
domain=mock.sentinel.domain)
def test_validation(self):
self.assertRaises(NotImplementedError, self.msg.validation, KEY)
class DNSTest(unittest.TestCase):
@@ -431,7 +366,7 @@ class DNSTest(unittest.TestCase):
mock_gen.return_value = mock.sentinel.validation
response = self.msg.gen_response(KEY)
from acme.challenges import DNSResponse
self.assertIsInstance(response, DNSResponse)
self.assertTrue(isinstance(response, DNSResponse))
self.assertEqual(response.validation, mock.sentinel.validation)
def test_validation_domain_name(self):
@@ -478,18 +413,5 @@ class DNSResponseTest(unittest.TestCase):
self.msg.check_validation(self.chall, KEY.public_key()))
class JWSPayloadRFC8555Compliant(unittest.TestCase):
"""Test for RFC8555 compliance of JWS generated from resources/challenges"""
def test_challenge_payload(self):
from acme.challenges import HTTP01Response
challenge_body = HTTP01Response()
challenge_body.le_acme_version = 2
jobj = challenge_body.json_dumps(indent=2).encode()
# RFC8555 states that challenge responses must have an empty payload.
self.assertEqual(jobj, b'{}')
if __name__ == '__main__':
unittest.main() # pragma: no cover

View File

@@ -2,21 +2,20 @@
# pylint: disable=too-many-lines
import copy
import datetime
import http.client as http_client
import json
import unittest
from typing import Dict
from unittest import mock
import josepy as jose
import mock
import OpenSSL
import requests
from six.moves import http_client # pylint: disable=import-error
from acme import challenges
from acme import errors
from acme import jws as acme_jws
from acme import messages
from acme.mixins import VersionedLEACMEMixin
from acme.magic_typing import Dict # pylint: disable=unused-import, no-name-in-module
import messages_test
import test_util
@@ -62,7 +61,7 @@ class ClientTestBase(unittest.TestCase):
self.contact = ('mailto:cert-admin@example.com', 'tel:+12025551212')
reg = messages.Registration(
contact=self.contact, key=KEY.public_key())
the_arg: Dict = dict(reg)
the_arg = dict(reg) # type: Dict
self.new_reg = messages.NewRegistration(**the_arg)
self.regr = messages.RegistrationResource(
body=reg, uri='https://www.letsencrypt-demo.org/acme/reg/1')
@@ -90,7 +89,7 @@ class BackwardsCompatibleClientV2Test(ClientTestBase):
"""Tests for acme.client.BackwardsCompatibleClientV2."""
def setUp(self):
super().setUp()
super(BackwardsCompatibleClientV2Test, self).setUp()
# contains a loaded cert
self.certr = messages.CertificateResource(
body=messages_test.CERT)
@@ -261,7 +260,7 @@ class BackwardsCompatibleClientV2Test(ClientTestBase):
with mock.patch('acme.client.ClientV2') as mock_client:
client = self._init()
client.finalize_order(mock_orderr, mock_deadline)
mock_client().finalize_order.assert_called_once_with(mock_orderr, mock_deadline, False)
mock_client().finalize_order.assert_called_once_with(mock_orderr, mock_deadline)
def test_revoke(self):
self.response.json.return_value = DIRECTORY_V1.to_json()
@@ -319,7 +318,7 @@ class ClientTest(ClientTestBase):
"""Tests for acme.client.Client."""
def setUp(self):
super().setUp()
super(ClientTest, self).setUp()
self.directory = DIRECTORY_V1
@@ -604,8 +603,8 @@ class ClientTest(ClientTestBase):
# make sure that max_attempts is per-authorization, rather
# than global
max_attempts=max(len(authzrs[0].retries), len(authzrs[1].retries)))
self.assertIs(cert[0], csr)
self.assertIs(cert[1], updated_authzrs)
self.assertTrue(cert[0] is csr)
self.assertTrue(cert[1] is updated_authzrs)
self.assertEqual(updated_authzrs[0].uri, 'a...')
self.assertEqual(updated_authzrs[1].uri, 'b.')
self.assertEqual(updated_authzrs[0].times, [
@@ -641,7 +640,7 @@ class ClientTest(ClientTestBase):
authzr = self.client.deactivate_authorization(self.authzr)
self.assertEqual(authzb, authzr.body)
self.assertEqual(self.client.net.post.call_count, 1)
self.assertIn(self.authzr.uri, self.net.post.call_args_list[0][0])
self.assertTrue(self.authzr.uri in self.net.post.call_args_list[0][0])
def test_check_cert(self):
self.response.headers['Location'] = self.certr.uri
@@ -700,7 +699,7 @@ class ClientTest(ClientTestBase):
def test_revocation_payload(self):
obj = messages.Revocation(certificate=self.certr.body, reason=self.rsn)
self.assertIn('reason', obj.to_partial_json().keys())
self.assertTrue('reason' in obj.to_partial_json().keys())
self.assertEqual(self.rsn, obj.to_partial_json()['reason'])
def test_revoke_bad_status_raises_error(self):
@@ -716,7 +715,7 @@ class ClientV2Test(ClientTestBase):
"""Tests for acme.client.ClientV2."""
def setUp(self):
super().setUp()
super(ClientV2Test, self).setUp()
self.directory = DIRECTORY_V2
@@ -840,32 +839,6 @@ class ClientV2Test(ClientTestBase):
deadline = datetime.datetime.now() - datetime.timedelta(seconds=60)
self.assertRaises(errors.TimeoutError, self.client.finalize_order, self.orderr, deadline)
def test_finalize_order_alt_chains(self):
updated_order = self.order.update(
certificate='https://www.letsencrypt-demo.org/acme/cert/',
)
updated_orderr = self.orderr.update(body=updated_order,
fullchain_pem=CERT_SAN_PEM,
alternative_fullchains_pem=[CERT_SAN_PEM,
CERT_SAN_PEM])
self.response.json.return_value = updated_order.to_json()
self.response.text = CERT_SAN_PEM
self.response.headers['Link'] ='<https://example.com/acme/cert/1>;rel="alternate", ' + \
'<https://example.com/dir>;rel="index", ' + \
'<https://example.com/acme/cert/2>;title="foo";rel="alternate"'
deadline = datetime.datetime(9999, 9, 9)
resp = self.client.finalize_order(self.orderr, deadline, fetch_alternative_chains=True)
self.net.post.assert_any_call('https://example.com/acme/cert/1',
mock.ANY, acme_version=2, new_nonce_url=mock.ANY)
self.net.post.assert_any_call('https://example.com/acme/cert/2',
mock.ANY, acme_version=2, new_nonce_url=mock.ANY)
self.assertEqual(resp, updated_orderr)
del self.response.headers['Link']
resp = self.client.finalize_order(self.orderr, deadline, fetch_alternative_chains=True)
self.assertEqual(resp, updated_orderr.update(alternative_fullchains_pem=[]))
def test_revoke(self):
self.client.revoke(messages_test.CERT, self.rsn)
self.net.post.assert_called_once_with(
@@ -877,9 +850,9 @@ class ClientV2Test(ClientTestBase):
self.response.headers['Location'] = self.regr.uri
self.response.json.return_value = self.regr.body.to_json()
self.assertEqual(self.regr, self.client.update_registration(self.regr))
self.assertIsNotNone(self.client.net.account)
self.assertNotEqual(self.client.net.account, None)
self.assertEqual(self.client.net.post.call_count, 2)
self.assertIn(DIRECTORY_V2.newAccount, self.net.post.call_args_list[0][0])
self.assertTrue(DIRECTORY_V2.newAccount in self.net.post.call_args_list[0][0])
self.response.json.return_value = self.regr.body.update(
contact=()).to_json()
@@ -913,7 +886,7 @@ class ClientV2Test(ClientTestBase):
self.client.net.get.assert_not_called()
class MockJSONDeSerializable(VersionedLEACMEMixin, jose.JSONDeSerializable):
class MockJSONDeSerializable(jose.JSONDeSerializable):
# pylint: disable=missing-docstring
def __init__(self, value):
self.value = value
@@ -943,7 +916,7 @@ class ClientNetworkTest(unittest.TestCase):
self.response.links = {}
def test_init(self):
self.assertIs(self.net.verify_ssl, self.verify_ssl)
self.assertTrue(self.net.verify_ssl is self.verify_ssl)
def test_wrap_in_jws(self):
# pylint: disable=protected-access
@@ -1185,7 +1158,7 @@ class ClientNetworkWithMockedResponseTest(unittest.TestCase):
def send_request(*args, **kwargs):
# pylint: disable=unused-argument,missing-docstring
self.assertNotIn("new_nonce_url", kwargs)
self.assertFalse("new_nonce_url" in kwargs)
method = args[0]
uri = args[1]
if method == 'HEAD' and uri != "new_nonce_uri":
@@ -1330,7 +1303,7 @@ class ClientNetworkSourceAddressBindingTest(unittest.TestCase):
from acme.client import ClientNetwork
net = ClientNetwork(key=None, alg=None, source_address=self.source_address)
for adapter in net.session.adapters.values():
self.assertIn(self.source_address, adapter.source_address)
self.assertTrue(self.source_address in adapter.source_address)
def test_behavior_assumption(self):
"""This is a test that guardrails the HTTPAdapter behavior so that if the default for
@@ -1340,7 +1313,7 @@ class ClientNetworkSourceAddressBindingTest(unittest.TestCase):
# test should fail if the default adapter type is changed by requests
net = ClientNetwork(key=None, alg=None)
session = requests.Session()
for scheme in session.adapters:
for scheme in session.adapters.keys():
client_network_adapter = net.session.adapters.get(scheme)
default_adapter = session.adapters.get(scheme)
self.assertEqual(client_network_adapter.__class__, default_adapter.__class__)

View File

@@ -1,22 +1,24 @@
"""Tests for acme.crypto_util."""
import itertools
import socket
import socketserver
import threading
import time
import unittest
from typing import List
import josepy as jose
import OpenSSL
import six
from six.moves import socketserver # type: ignore # pylint: disable=import-error
from acme import errors
from acme.magic_typing import List # pylint: disable=unused-import, no-name-in-module
import test_util
class SSLSocketAndProbeSNITest(unittest.TestCase):
"""Tests for acme.crypto_util.SSLSocket/probe_sni."""
def setUp(self):
self.cert = test_util.load_comparable_cert('rsa2048_cert.pem')
key = test_util.load_pyopenssl_private_key('rsa2048_key.pem')
@@ -27,9 +29,10 @@ class SSLSocketAndProbeSNITest(unittest.TestCase):
class _TestServer(socketserver.TCPServer):
# six.moves.* | pylint: disable=attribute-defined-outside-init,no-init
def server_bind(self): # pylint: disable=missing-docstring
self.socket = SSLSocket(socket.socket(),
certs)
self.socket = SSLSocket(socket.socket(), certs=certs)
socketserver.TCPServer.server_bind(self)
self.server = _TestServer(('', 0), socketserver.BaseRequestHandler)
@@ -60,6 +63,7 @@ class SSLSocketAndProbeSNITest(unittest.TestCase):
self.assertRaises(errors.Error, self._probe, b'bar')
def test_probe_connection_error(self):
# pylint has a hard time with six
self.server.server_close()
original_timeout = socket.getdefaulttimeout()
try:
@@ -69,18 +73,6 @@ class SSLSocketAndProbeSNITest(unittest.TestCase):
socket.setdefaulttimeout(original_timeout)
class SSLSocketTest(unittest.TestCase):
"""Tests for acme.crypto_util.SSLSocket."""
def test_ssl_socket_invalid_arguments(self):
from acme.crypto_util import SSLSocket
with self.assertRaises(ValueError):
_ = SSLSocket(None, {'sni': ('key', 'cert')},
cert_selection=lambda _: None)
with self.assertRaises(ValueError):
_ = SSLSocket(None)
class PyOpenSSLCertOrReqAllNamesTest(unittest.TestCase):
"""Test for acme.crypto_util._pyopenssl_cert_or_req_all_names."""
@@ -118,9 +110,9 @@ class PyOpenSSLCertOrReqSANTest(unittest.TestCase):
@classmethod
def _get_idn_names(cls):
"""Returns expected names from '{cert,csr}-idnsans.pem'."""
chars = [chr(i) for i in itertools.chain(range(0x3c3, 0x400),
range(0x641, 0x6fc),
range(0x1820, 0x1877))]
chars = [six.unichr(i) for i in itertools.chain(range(0x3c3, 0x400),
range(0x641, 0x6fc),
range(0x1820, 0x1877))]
return [''.join(chars[i: i + 45]) + '.invalid'
for i in range(0, len(chars), 45)]
@@ -181,7 +173,7 @@ class RandomSnTest(unittest.TestCase):
def setUp(self):
self.cert_count = 5
self.serial_num: List[int] = []
self.serial_num = [] # type: List[int]
self.key = OpenSSL.crypto.PKey()
self.key.generate_key(OpenSSL.crypto.TYPE_RSA, 2048)
@@ -191,7 +183,7 @@ class RandomSnTest(unittest.TestCase):
for _ in range(self.cert_count):
cert = gen_ss_cert(self.key, ['dummy'], force_san=True)
self.serial_num.append(cert.get_serial_number())
self.assertGreater(len(set(self.serial_num)), 1)
self.assertTrue(len(set(self.serial_num)) > 1)
class MakeCSRTest(unittest.TestCase):
"""Test for standalone functions."""
@@ -206,8 +198,8 @@ class MakeCSRTest(unittest.TestCase):
def test_make_csr(self):
csr_pem = self._call_with_key(["a.example", "b.example"])
self.assertIn(b'--BEGIN CERTIFICATE REQUEST--', csr_pem)
self.assertIn(b'--END CERTIFICATE REQUEST--', csr_pem)
self.assertTrue(b'--BEGIN CERTIFICATE REQUEST--' in csr_pem)
self.assertTrue(b'--END CERTIFICATE REQUEST--' in csr_pem)
csr = OpenSSL.crypto.load_certificate_request(
OpenSSL.crypto.FILETYPE_PEM, csr_pem)
# In pyopenssl 0.13 (used with TOXENV=py27-oldest), csr objects don't

View File

@@ -1,6 +1,7 @@
"""Tests for acme.errors."""
import unittest
from unittest import mock
import mock
class BadNonceTest(unittest.TestCase):
@@ -24,8 +25,8 @@ class MissingNonceTest(unittest.TestCase):
self.error = MissingNonce(self.response)
def test_str(self):
self.assertIn("FOO", str(self.error))
self.assertIn("{}", str(self.error))
self.assertTrue("FOO" in str(self.error))
self.assertTrue("{}" in str(self.error))
class PollErrorTest(unittest.TestCase):
@@ -34,7 +35,7 @@ class PollErrorTest(unittest.TestCase):
def setUp(self):
from acme.errors import PollError
self.timeout = PollError(
exhausted={mock.sentinel.AR},
exhausted=set([mock.sentinel.AR]),
updated={})
self.invalid = PollError(exhausted=set(), updated={
mock.sentinel.AR: mock.sentinel.AR2})

View File

@@ -48,7 +48,7 @@ class JWSTest(unittest.TestCase):
self.assertEqual(jws.signature.combined.nonce, self.nonce)
self.assertEqual(jws.signature.combined.url, self.url)
self.assertEqual(jws.signature.combined.kid, self.kid)
self.assertIsNone(jws.signature.combined.jwk)
self.assertEqual(jws.signature.combined.jwk, None)
# TODO: check that nonce is in protected header
self.assertEqual(jws, JWS.from_json(jws.to_json()))
@@ -58,7 +58,7 @@ class JWSTest(unittest.TestCase):
jws = JWS.sign(payload=b'foo', key=self.privkey,
alg=jose.RS256, nonce=self.nonce,
url=self.url)
self.assertIsNone(jws.signature.combined.kid)
self.assertEqual(jws.signature.combined.kid, None)
self.assertEqual(jws.signature.combined.jwk, self.pubkey)

View File

@@ -1,8 +1,8 @@
"""Tests for acme.magic_typing."""
import sys
import unittest
import warnings
from unittest import mock
import mock
class MagicTypingTest(unittest.TestCase):
@@ -10,21 +10,32 @@ class MagicTypingTest(unittest.TestCase):
def test_import_success(self):
try:
import typing as temp_typing
except ImportError: # pragma: no cover
temp_typing = None # pragma: no cover
except ImportError: # pragma: no cover
temp_typing = None # pragma: no cover
typing_class_mock = mock.MagicMock()
text_mock = mock.MagicMock()
typing_class_mock.Text = text_mock
sys.modules['typing'] = typing_class_mock
if 'acme.magic_typing' in sys.modules:
del sys.modules['acme.magic_typing'] # pragma: no cover
with warnings.catch_warnings():
warnings.filterwarnings("ignore", category=DeprecationWarning)
from acme.magic_typing import Text
del sys.modules['acme.magic_typing'] # pragma: no cover
from acme.magic_typing import Text # pylint: disable=no-name-in-module
self.assertEqual(Text, text_mock)
del sys.modules['acme.magic_typing']
sys.modules['typing'] = temp_typing
def test_import_failure(self):
try:
import typing as temp_typing
except ImportError: # pragma: no cover
temp_typing = None # pragma: no cover
sys.modules['typing'] = None
if 'acme.magic_typing' in sys.modules:
del sys.modules['acme.magic_typing'] # pragma: no cover
from acme.magic_typing import Text # pylint: disable=no-name-in-module
self.assertTrue(Text is None)
del sys.modules['acme.magic_typing']
sys.modules['typing'] = temp_typing
if __name__ == '__main__':
unittest.main() # pragma: no cover

View File

@@ -1,11 +1,11 @@
"""Tests for acme.messages."""
from typing import Dict
import unittest
from unittest import mock
import josepy as jose
import mock
from acme import challenges
from acme.magic_typing import Dict # pylint: disable=unused-import, no-name-in-module
import test_util
CERT = test_util.load_comparable_cert('cert.der')
@@ -41,13 +41,13 @@ class ErrorTest(unittest.TestCase):
def test_description(self):
self.assertEqual('The request message was malformed', self.error.description)
self.assertIsNone(self.error_custom.description)
self.assertTrue(self.error_custom.description is None)
def test_code(self):
from acme.messages import Error
self.assertEqual('malformed', self.error.code)
self.assertIsNone(self.error_custom.code)
self.assertIsNone(Error().code)
self.assertEqual(None, self.error_custom.code)
self.assertEqual(None, Error().code)
def test_is_acme_error(self):
from acme.messages import is_acme_error, Error
@@ -82,7 +82,7 @@ class ConstantTest(unittest.TestCase):
from acme.messages import _Constant
class MockConstant(_Constant): # pylint: disable=missing-docstring
POSSIBLE_NAMES: Dict = {}
POSSIBLE_NAMES = {} # type: Dict
self.MockConstant = MockConstant # pylint: disable=invalid-name
self.const_a = MockConstant('a')
@@ -106,11 +106,11 @@ class ConstantTest(unittest.TestCase):
def test_equality(self):
const_a_prime = self.MockConstant('a')
self.assertNotEqual(self.const_a, self.const_b)
self.assertEqual(self.const_a, const_a_prime)
self.assertFalse(self.const_a == self.const_b)
self.assertTrue(self.const_a == const_a_prime)
self.assertNotEqual(self.const_a, self.const_b)
self.assertEqual(self.const_a, const_a_prime)
self.assertTrue(self.const_a != self.const_b)
self.assertFalse(self.const_a != const_a_prime)
class DirectoryTest(unittest.TestCase):
@@ -252,19 +252,6 @@ class RegistrationTest(unittest.TestCase):
from acme.messages import Registration
hash(Registration.from_json(self.jobj_from))
def test_default_not_transmitted(self):
from acme.messages import NewRegistration
empty_new_reg = NewRegistration()
new_reg_with_contact = NewRegistration(contact=())
self.assertEqual(empty_new_reg.contact, ())
self.assertEqual(new_reg_with_contact.contact, ())
self.assertNotIn('contact', empty_new_reg.to_partial_json())
self.assertNotIn('contact', empty_new_reg.fields_to_partial_json())
self.assertIn('contact', new_reg_with_contact.to_partial_json())
self.assertIn('contact', new_reg_with_contact.fields_to_partial_json())
class UpdateRegistrationTest(unittest.TestCase):
"""Tests for acme.messages.UpdateRegistration."""
@@ -406,7 +393,7 @@ class AuthorizationResourceTest(unittest.TestCase):
authzr = AuthorizationResource(
uri=mock.sentinel.uri,
body=mock.sentinel.body)
self.assertIsInstance(authzr, jose.JSONDeSerializable)
self.assertTrue(isinstance(authzr, jose.JSONDeSerializable))
class CertificateRequestTest(unittest.TestCase):
@@ -417,7 +404,7 @@ class CertificateRequestTest(unittest.TestCase):
self.req = CertificateRequest(csr=CSR)
def test_json_de_serializable(self):
self.assertIsInstance(self.req, jose.JSONDeSerializable)
self.assertTrue(isinstance(self.req, jose.JSONDeSerializable))
from acme.messages import CertificateRequest
self.assertEqual(
self.req, CertificateRequest.from_json(self.req.to_json()))
@@ -433,7 +420,7 @@ class CertificateResourceTest(unittest.TestCase):
cert_chain_uri=mock.sentinel.cert_chain_uri)
def test_json_de_serializable(self):
self.assertIsInstance(self.certr, jose.JSONDeSerializable)
self.assertTrue(isinstance(self.certr, jose.JSONDeSerializable))
from acme.messages import CertificateResource
self.assertEqual(
self.certr, CertificateResource.from_json(self.certr.to_json()))
@@ -466,7 +453,6 @@ class OrderResourceTest(unittest.TestCase):
'authorizations': None,
})
class NewOrderTest(unittest.TestCase):
"""Tests for acme.messages.NewOrder."""
@@ -481,18 +467,5 @@ class NewOrderTest(unittest.TestCase):
})
class JWSPayloadRFC8555Compliant(unittest.TestCase):
"""Test for RFC8555 compliance of JWS generated from resources/challenges"""
def test_message_payload(self):
from acme.messages import NewAuthorization
new_order = NewAuthorization()
new_order.le_acme_version = 2
jobj = new_order.json_dumps(indent=2).encode()
# RFC8555 states that JWS bodies must not have a resource field.
self.assertEqual(jobj, b'{}')
if __name__ == '__main__':
unittest.main() # pragma: no cover

View File

@@ -1,19 +1,16 @@
"""Tests for acme.standalone."""
import http.client as http_client
import socket
import socketserver
import threading
import unittest
from typing import Set
from unittest import mock
import josepy as jose
import mock
import requests
from six.moves import http_client # pylint: disable=import-error
from six.moves import socketserver # type: ignore # pylint: disable=import-error
from acme import challenges
from acme import crypto_util
from acme import errors
from acme.magic_typing import Set # pylint: disable=unused-import, no-name-in-module
import test_util
@@ -42,7 +39,7 @@ class HTTP01ServerTest(unittest.TestCase):
def setUp(self):
self.account_key = jose.JWK.load(
test_util.load_vector('rsa1024_key.pem'))
self.resources: Set = set()
self.resources = set() # type: Set
from acme.standalone import HTTP01Server
self.server = HTTP01Server(('', 0), resources=self.resources)
@@ -86,81 +83,6 @@ class HTTP01ServerTest(unittest.TestCase):
def test_http01_not_found(self):
self.assertFalse(self._test_http01(add=False))
def test_timely_shutdown(self):
from acme.standalone import HTTP01Server
server = HTTP01Server(('', 0), resources=set(), timeout=0.05)
server_thread = threading.Thread(target=server.serve_forever)
server_thread.start()
client = socket.socket()
client.connect(('localhost', server.socket.getsockname()[1]))
stop_thread = threading.Thread(target=server.shutdown)
stop_thread.start()
server_thread.join(5.)
is_hung = server_thread.is_alive()
try:
client.shutdown(socket.SHUT_RDWR)
except: # pragma: no cover, pylint: disable=bare-except
# may raise error because socket could already be closed
pass
self.assertFalse(is_hung, msg='Server shutdown should not be hung')
@unittest.skipIf(not challenges.TLSALPN01.is_supported(), "pyOpenSSL too old")
class TLSALPN01ServerTest(unittest.TestCase):
"""Test for acme.standalone.TLSALPN01Server."""
def setUp(self):
self.certs = {b'localhost': (
test_util.load_pyopenssl_private_key('rsa2048_key.pem'),
test_util.load_cert('rsa2048_cert.pem'),
)}
# Use different certificate for challenge.
self.challenge_certs = {b'localhost': (
test_util.load_pyopenssl_private_key('rsa4096_key.pem'),
test_util.load_cert('rsa4096_cert.pem'),
)}
from acme.standalone import TLSALPN01Server
self.server = TLSALPN01Server(("localhost", 0), certs=self.certs,
challenge_certs=self.challenge_certs)
# pylint: disable=no-member
self.thread = threading.Thread(target=self.server.serve_forever)
self.thread.start()
def tearDown(self):
self.server.shutdown() # pylint: disable=no-member
self.thread.join()
# TODO: This is not implemented yet, see comments in standalone.py
# def test_certs(self):
# host, port = self.server.socket.getsockname()[:2]
# cert = crypto_util.probe_sni(
# b'localhost', host=host, port=port, timeout=1)
# # Expect normal cert when connecting without ALPN.
# self.assertEqual(jose.ComparableX509(cert),
# jose.ComparableX509(self.certs[b'localhost'][1]))
def test_challenge_certs(self):
host, port = self.server.socket.getsockname()[:2]
cert = crypto_util.probe_sni(
b'localhost', host=host, port=port, timeout=1,
alpn_protocols=[b"acme-tls/1"])
# Expect challenge cert when connecting with ALPN.
self.assertEqual(
jose.ComparableX509(cert),
jose.ComparableX509(self.challenge_certs[b'localhost'][1])
)
def test_bad_alpn(self):
host, port = self.server.socket.getsockname()[:2]
with self.assertRaises(errors.Error):
crypto_util.probe_sni(
b'localhost', host=host, port=port, timeout=1,
alpn_protocols=[b"bad-alpn"])
class BaseDualNetworkedServersTest(unittest.TestCase):
"""Test for acme.standalone.BaseDualNetworkedServers."""
@@ -216,10 +138,11 @@ class BaseDualNetworkedServersTest(unittest.TestCase):
class HTTP01DualNetworkedServersTest(unittest.TestCase):
"""Tests for acme.standalone.HTTP01DualNetworkedServers."""
def setUp(self):
self.account_key = jose.JWK.load(
test_util.load_vector('rsa1024_key.pem'))
self.resources: Set = set()
self.resources = set() # type: Set
from acme.standalone import HTTP01DualNetworkedServers
self.servers = HTTP01DualNetworkedServers(('', 0), resources=self.resources)

View File

@@ -4,14 +4,12 @@ to use appropriate extension for vector filenames: .pem for PEM and
The following command has been used to generate test keys:
for k in 256 512 1024 2048 4096; do openssl genrsa -out rsa${k}_key.pem $k; done
for x in 256 512 1024 2048; do openssl genrsa -out rsa${k}_key.pem $k; done
and for the CSR:
openssl req -key rsa2048_key.pem -new -subj '/CN=example.com' -outform DER > csr.der
and for the certificates:
and for the certificate:
openssl req -key rsa2048_key.pem -new -subj '/CN=example.com' -x509 -outform DER > cert.der
openssl req -key rsa2048_key.pem -new -subj '/CN=example.com' -x509 > rsa2048_cert.pem
openssl req -key rsa1024_key.pem -new -subj '/CN=example.com' -x509 > rsa1024_cert.pem
openssl req -key rsa2047_key.pem -new -subj '/CN=example.com' -x509 -outform DER > cert.der

View File

@@ -1,13 +0,0 @@
-----BEGIN CERTIFICATE-----
MIIB/TCCAWagAwIBAgIJAOyRIBs3QT8QMA0GCSqGSIb3DQEBCwUAMBYxFDASBgNV
BAMMC2V4YW1wbGUuY29tMB4XDTE4MDQyMzEwMzE0NFoXDTE4MDUyMzEwMzE0NFow
FjEUMBIGA1UEAwwLZXhhbXBsZS5jb20wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJ
AoGBAJqJ87R8aVwByONxgQA9hwgvQd/QqI1r1UInXhEF2VnEtZGtUWLi100IpIqr
Mq4qusDwNZ3g8cUPtSkvJGs89djoajMDIJP7lQUEKUYnYrI0q755Tr/DgLWSk7iW
l5ezym0VzWUD0/xXUz8yRbNMTjTac80rS5SZk2ja2wWkYlRJAgMBAAGjUzBRMB0G
A1UdDgQWBBSsaX0IVZ4XXwdeffVAbG7gnxSYjTAfBgNVHSMEGDAWgBSsaX0IVZ4X
XwdeffVAbG7gnxSYjTAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4GB
ADe7SVmvGH2nkwVfONk8TauRUDkePN1CJZKFb2zW1uO9ANJ2v5Arm/OQp0BG/xnI
Djw/aLTNVESF89oe15dkrUErtcaF413MC1Ld5lTCaJLHLGqDKY69e02YwRuxW7jY
qarpt7k7aR5FbcfO5r4V/FK/Gvp4Dmoky8uap7SJIW6x
-----END CERTIFICATE-----

View File

@@ -1,30 +0,0 @@
-----BEGIN CERTIFICATE-----
MIIFDTCCAvWgAwIBAgIUImqDrP53V69vFROsjP/gL0YtoA4wDQYJKoZIhvcNAQEL
BQAwFjEUMBIGA1UEAwwLZXhhbXBsZS5jb20wHhcNMjAwNTI3MjMyNDE0WhcNMjAw
NjI2MjMyNDE0WjAWMRQwEgYDVQQDDAtleGFtcGxlLmNvbTCCAiIwDQYJKoZIhvcN
AQEBBQADggIPADCCAgoCggIBANY9LKLk9Dxn0MUMQFHwBoTN4ehDSWBws2KcytpF
mc8m9Mfk1wmb4fQSKYtK3wIFMfIyo9HQu0nKqMkkUw52o3ZXyOv+oWwF5qNy2BKu
lh5OMSkaZ0o13zoPpW42e+IUnyxvg70+0urD+sUue4cyTHh/nBIUjrM/05ZJ/ac8
HR0RK3H41YoqBjq69JjMZczZZhbNFit3s6p0R1TbVAgc3ckqbtX5BDyQMQQCP4Ed
m4DgbAFVqdcPUCC5W3F3fmuQiPKHiADzONZnXpy6lUvLDWqcd6loKp+nKHM6OkXX
8hmD7pE1PYMQo4hqOfhBR2IgMjAShwd5qUFjl1m2oo0Qm3PFXOk6i2ZQdS6AA/yd
B5/mX0RnM2oIdFZPb6UZFSmtEgs9sTzn+hMUyNSZQRE54px1ur1xws2R+vbsCyM5
+KoFVxDjVjU9TlZx3GvDvnqz/tbHjji6l8VHZYOBMBUXbKHu2U6pJFZ5Zp7k68/z
a3Fb9Pjtn3iRkXEyC0N5kLgqO4QTlExnxebV8aMvQpWd/qefnMn9qPYIZPEXSQAR
mEBIahkcACb60s+acG0WFFluwBPtBqEr8Q67XlSF0Ibf4iBiRzpPobhlWta1nrFg
4IWHMSoZ0PE75bhIGBEkhrpcXQCAxXmAfxfjKDH7jdJ1fRdnZ/9+OzwYGVX5GH/l
0QDtAgMBAAGjUzBRMB0GA1UdDgQWBBQh3xiz/o1nEU2ySylZ9gxCXvIPGzAfBgNV
HSMEGDAWgBQh3xiz/o1nEU2ySylZ9gxCXvIPGzAPBgNVHRMBAf8EBTADAQH/MA0G
CSqGSIb3DQEBCwUAA4ICAQAELoXz31oR9pdAwidlv9ZBOKiC7KBWy8VMqXNVkfTn
bVRxAUex7zleLFIOkWnqadsMesU9sIwrbLzBcZ8Q/vBY+z2xOPdXcgcAoAmdKWoq
YBQNiqng9r54sqlzB/77QZCf5fdktESe7NTxhCifgx5SAWq7IUQs/lm3tnMUSAfE
5ctuN6M+w8K54y3WDprcfMHpnc3ZHeSPhVQApHM0h/bDvXq0bRS7kmq27Hb153Qm
nH3TwYB5pPSWW38NbUc+s/a7mItO7S8ly8yGbA0j9c/IbN5lM+OCdk06asz3+c8E
uo8nuCBoYO5+6AqC2N7WJ3Tdr/pFA8jTbd6VNVlgCWTIR8ZosL5Fgkfv+4fUBrHt
zdVUqMUzvga5rvZnwnJ5Qfu/drHeAAo9MTNFQNe2QgDlYfWBh5GweolgmFSwrpkY
v/5wLtIyv/ASHKswybbqMIlpttcLTXjx5yuh8swttT6Wh+FQqqQ32KSRB3StiwyK
oH0ZhrwYHiFYNlPxecGX6XUta6rFtTlEdkBGSnXzgiTzL2l+Nc0as0V5B9RninZG
qJ+VOChSQ0OFvg1riSXv7tMvbLdGQnxwTRL3t6BMS8I4LA2m3ZfWUcuXT783ODTH
16f1Q1AgXd2csstTWO9cv+N/0fpX31nqrm6+CrGduSr2u4HjYYnlLIUhmdTvK3fX
Fg==
-----END CERTIFICATE-----

View File

@@ -1,51 +0,0 @@
-----BEGIN RSA PRIVATE KEY-----
MIIJKgIBAAKCAgEA1j0souT0PGfQxQxAUfAGhM3h6ENJYHCzYpzK2kWZzyb0x+TX
CZvh9BIpi0rfAgUx8jKj0dC7ScqoySRTDnajdlfI6/6hbAXmo3LYEq6WHk4xKRpn
SjXfOg+lbjZ74hSfLG+DvT7S6sP6xS57hzJMeH+cEhSOsz/Tlkn9pzwdHRErcfjV
iioGOrr0mMxlzNlmFs0WK3ezqnRHVNtUCBzdySpu1fkEPJAxBAI/gR2bgOBsAVWp
1w9QILlbcXd+a5CI8oeIAPM41mdenLqVS8sNapx3qWgqn6coczo6RdfyGYPukTU9
gxCjiGo5+EFHYiAyMBKHB3mpQWOXWbaijRCbc8Vc6TqLZlB1LoAD/J0Hn+ZfRGcz
agh0Vk9vpRkVKa0SCz2xPOf6ExTI1JlBETninHW6vXHCzZH69uwLIzn4qgVXEONW
NT1OVnHca8O+erP+1seOOLqXxUdlg4EwFRdsoe7ZTqkkVnlmnuTrz/NrcVv0+O2f
eJGRcTILQ3mQuCo7hBOUTGfF5tXxoy9ClZ3+p5+cyf2o9ghk8RdJABGYQEhqGRwA
JvrSz5pwbRYUWW7AE+0GoSvxDrteVIXQht/iIGJHOk+huGVa1rWesWDghYcxKhnQ
8TvluEgYESSGulxdAIDFeYB/F+MoMfuN0nV9F2dn/347PBgZVfkYf+XRAO0CAwEA
AQKCAgEA0hZdTkQtCYtYm9LexDsXeWYX8VcCfrMmBj7xYcg9A3oVMmzDPuYBVwH0
gWbjd6y2hOaJ5TfGYZ99kvmvBRDsTSHaoyopC7BhssjtAKz6Ay/0X3VH8usPQ3WS
aZi+NT65tK6KRqtz08ppgLGLa1G00bl5x/Um1rpxeACI4FU/y4BJ1VMJvJpnT3KE
Z86Qyagqx5NH+UpCApZSWPFX3zjHePzGgcfXErjniCHYOnpZQrFQ2KIzkfSvQ9fg
x01ByKOM2CB2C1B33TCzBAioXRH6zyAu7A59NeCK9ywTduhDvie1a+oEryFC7IQW
4s7I/H3MGX4hsf/pLXlHMy+5CZJOjRaC2h+pypfbbcuiXu6Sn64kHNpiI7SxI5DI
MIRjyG7MdUcrzq0Rt8ogwwpbCoRqrl/w3bhxtqmeZaEZtyxbjlm7reK2YkIFDgyz
JMqiJK5ZAi+9L/8c0xhjjAQQ0sIzrjmjA8U+6YnWL9jU5qXTVnBB8XQucyeeZGgk
yRHyMur71qOXN8z3UEva7MHkDTUBlj8DgTz6sEjqCipaWl0CXfDNa4IhHIXD5qiF
wplhq7OeS0v6EGG/UFa3Q/lFntxtrayxJX7uvvSccGzjPKXTjpWUELLi/FdnIsum
eXT3RgIEYozj4BibDXaBLfHTCVzxOr7AAEvKM9XWSUgLA0paSWECggEBAO9ZBeE1
GWzd1ejTTkcxBC9AK2rNsYG8PdNqiof/iTbuJWNeRqpG+KB/0CNIpjZ2X5xZd0tM
FDpHTFehlP26Roxuq50iRAFc+SN5KoiO0A3JuJAidreIgRTia1saUUrypHqWrYEA
VZVj2AI8Pyg3s1OkR2frFskY7hXBVb/pJNDP/m9xTXXIYiIXYkHYe+4RIJCnAxRv
q5YHKaX+0Ull9YCZJCxmwvcHat8sgu8qkiwUMEM6QSNEkrEbdnWYBABvC1AR6sws
7MP1h9+j22n4Zc/3D6kpFZEL9Erx8nNyhbOZ6q2Tdnf6YKVVjZdyVa8VyNnR0ROl
3BjkFaHb/bg4e4kCggEBAOUk8ZJS3qBeGCOjug384zbHGcnhUBYtYJiOz+RXBtP+
PRksbFtTkgk1sHuSGO8YRddU4Qv7Av1xL8o+DEsLBSD0YQ7pmLrR/LK+iDQ5N63O
Fve9uJH0ybxAOkiua7G24+lTsVUP//KWToL4Wh5zbHBBjL5D2Z9zoeVbcE87xhva
lImMVr4Ex252DqNP9wkZxBjudFyJ/C/TnXrjPcgwhxWTC7sLQMhE5p+490G7c4hX
PywkIKrANbu37KDiAvVS+dC66ZgpL/NUDkeloAmGNO08LGzbV6YKchlvDyWU/AvW
0hYjbL0FUq7K/wp1G9fumolB+fbI25K9c13X93STzUUCggEBAJDsNFUyk5yJjbYW
C/WrRj9d+WwH9Az77+uNPSgvn+O0usq6EMuVgYGdImfa21lqv2Wp/kOHY1AOT7lX
yyD+oyzw7dSNJOQ2aVwDR6+72Vof5DLRy1RBwPbmSd61xrc8yD658YCEtU1pUSe5
VvyBDYH9nIbdn8RP5gkiMUusXXBaIFNWJXLFzDWcNxBrhk6V7EPp/EFphFmpKJyr
+AkbRVWCZJbF+hMdWKadCwLJogwyhS6PnVU/dhrq6AU38GRa2Fy5HJRYN1xH1Oej
DX3Su8L6c28Xw0k6FcczTHx+wVoIPkKvYTIwVkiFzt/+iMckx6KsGo5tBSHFKRwC
WlQrTxECggEBALjUruLnY1oZ7AC7bTUhOimSOfQEgTQSUCtebsRxijlvhtsKYTDd
XRt+qidStjgN7S/+8DRYuZWzOeg5WnMhpXZqiOudcyume922IGl3ibjxVsdoyjs5
J4xohlrgDlBgBMDNWGoTqNGFejjcmNydH+gAh8VlN2INxJYbxqCyx17qVgwJHmLR
uggYxD/pHYvCs9GkbknCp5/wYsOgDtKuihfV741lS1D/esN1UEQ+LrfYIEW7snno
5q7Pcdhn1hkKYCWEzy2Ec4Aj2gzixQ9JqOF/OxpnZvCw1k47rg0TeqcWFYnz8x8Y
7xO8/DH0OoxXk2GJzVXJuItJs4gLzzfCjL0CggEAJFHfC9jisdy7CoWiOpNCSF1B
S0/CWDz77cZdlWkpTdaXGGp1MA/UKUFPIH8sOHfvpKS660+X4G/1ZBHmFb4P5kFF
Qy8UyUMKtSOEdZS6KFlRlfSCAMd5aSTmCvq4OSjYEpMRwUhU/iEJNkn9Z1Soehe0
U3dxJ8KiT1071geO6rRquSHoSJs6Y0WQKriYYQJOhh4Axs3PQihER2eyh+WGk8YJ
02m0mMsjntqnXtdc6IcdKaHp9ko+OpM9QZLsvt19fxBcrXj/i21uUXrzuNtKfO6M
JqGhsOrO2dh8lMhvodENvgKA0DmYDC9N7ogo7bxTNSedcjBF46FhJoqii8m70Q==
-----END RSA PRIVATE KEY-----

View File

@@ -1,7 +1,7 @@
include LICENSE.txt
include README.rst
recursive-include tests *
include certbot_apache/_internal/options-ssl-apache.conf
recursive-include certbot_apache/_internal/augeas_lens *.aug
recursive-include certbot_apache/_internal/tls_configs *.conf
global-exclude __pycache__
global-exclude *.py[cod]

View File

@@ -5,10 +5,9 @@ import logging
import re
import subprocess
import pkg_resources
from certbot import errors
from certbot import util
from certbot.compat import os
logger = logging.getLogger(__name__)
@@ -129,7 +128,7 @@ def included_in_paths(filepath, paths):
:rtype: bool
"""
return any(fnmatch.fnmatch(filepath, path) for path in paths)
return any([fnmatch.fnmatch(filepath, path) for path in paths])
def parse_defines(apachectl):
@@ -143,7 +142,7 @@ def parse_defines(apachectl):
:rtype: dict
"""
variables = {}
variables = dict()
define_cmd = [apachectl, "-t", "-D",
"DUMP_RUN_CFG"]
matches = parse_from_subprocess(define_cmd, r"Define: ([^ \n]*)")
@@ -220,14 +219,12 @@ def _get_runtime_cfg(command):
"""
try:
proc = subprocess.run(
proc = subprocess.Popen(
command,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
universal_newlines=True,
check=False,
env=util.env_no_snap_for_external_calls())
stdout, stderr = proc.stdout, proc.stderr
universal_newlines=True)
stdout, stderr = proc.communicate()
except (OSError, ValueError):
logger.error(
@@ -244,14 +241,3 @@ def _get_runtime_cfg(command):
"loaded because Apache is misconfigured.")
return stdout
def find_ssl_apache_conf(prefix):
"""
Find a TLS Apache config file in the dedicated storage.
:param str prefix: prefix of the TLS Apache config file to find
:return: the path the TLS Apache config file
:rtype: str
"""
return pkg_resources.resource_filename(
"certbot_apache",
os.path.join("_internal", "tls_configs", "{0}-options-ssl-apache.conf".format(prefix)))

View File

@@ -1,5 +1,10 @@
""" apacheconfig implementation of the ParserNode interfaces """
from typing import Tuple
import glob
from acme.magic_typing import Optional # pylint: disable=unused-import, no-name-in-module
from certbot.compat import os
from certbot_apache._internal import assertions
from certbot_apache._internal import interfaces
@@ -15,14 +20,14 @@ class ApacheParserNode(interfaces.ParserNode):
def __init__(self, **kwargs):
ancestor, dirty, filepath, metadata = util.parsernode_kwargs(kwargs) # pylint: disable=unused-variable
super().__init__(**kwargs)
super(ApacheParserNode, self).__init__(**kwargs)
self.ancestor = ancestor
self.filepath = filepath
self.dirty = dirty
self.metadata = metadata
self._raw = self.metadata["ac_ast"]
def save(self, msg): # pragma: no cover
def save(self, msg): # pragma: no cover
pass
def find_ancestors(self, name): # pylint: disable=unused-variable
@@ -39,7 +44,7 @@ class ApacheCommentNode(ApacheParserNode):
def __init__(self, **kwargs):
comment, kwargs = util.commentnode_kwargs(kwargs) # pylint: disable=unused-variable
super().__init__(**kwargs)
super(ApacheCommentNode, self).__init__(**kwargs)
self.comment = comment
def __eq__(self, other): # pragma: no cover
@@ -52,17 +57,46 @@ class ApacheCommentNode(ApacheParserNode):
return False
def _load_included_file(filename, metadata):
with open(filename) as f:
ast = metadata['loader'].loads(f.read())
metadata = metadata.copy()
metadata['ac_ast'] = ast
return ApacheBlockNode(name=assertions.PASS,
ancestor=None,
filepath=filename,
metadata=metadata)
class ApacheDirectiveNode(ApacheParserNode):
""" apacheconfig implementation of DirectiveNode interface """
def __init__(self, **kwargs):
name, parameters, enabled, kwargs = util.directivenode_kwargs(kwargs)
super().__init__(**kwargs)
super(ApacheDirectiveNode, self).__init__(**kwargs)
self.name = name
self.parameters = parameters
self.enabled = enabled
self.include = None
# LoadModule processing
if self.name and self.name.lower() in ["loadmodule"]:
mod_name, mod_filename = self.parameters
self.metadata["apache_vars"]["modules"].add(mod_name)
self.metadata["apache_vars"]["modules"].add(
os.path.basename(mod_filename)[:-2] + "c")
# Include processing
if self.name and self.name.lower() in ["include", "includeoptional"]:
value = self.parameters[0]
path = os.path.join(self.metadata['serverroot'], value)
filepaths = glob.glob(path)
for filepath in filepaths:
if filepath not in self.metadata['parsed_files']:
node = _load_included_file(filepath, self.metadata)
self.metadata['parsed_files'][filepath] = node
self.include = set(filepaths)
def __eq__(self, other): # pragma: no cover
if isinstance(other, self.__class__):
return (self.name == other.name and
@@ -74,17 +108,93 @@ class ApacheDirectiveNode(ApacheParserNode):
self.metadata == other.metadata)
return False
def set_parameters(self, _parameters): # pragma: no cover
def set_parameters(self, _parameters):
"""Sets the parameters for DirectiveNode"""
return
self.parameters = tuple(_parameters)
self._raw.value = tuple(" ".join(_parameters))
def _recursive_generator(node, exclude=True, files_visited=None):
"""Recursive generator over all children of given block node, expanding includes.
:param ApacheBlockNode node: node whose children will be yielded by this generator
:param bool exclude: If True, excludes nodes disabled by conditional blocks like
IfDefine and IfModule.
:param dict files_visited: bookkeeping dict for recursion to ensure we don't visit
the same file twice (to avoid double-counting nodes)
"""
if not files_visited:
files_visited = set([node.filepath])
for child in node.children:
yield child
if isinstance(child, ApacheBlockNode):
if not exclude or child.enabled:
for subchild in _recursive_generator(child, exclude, files_visited):
yield subchild
if isinstance(child, ApacheDirectiveNode) and child.include:
for filename in child.include:
if filename not in files_visited:
files_visited.add(filename)
file_ast = node.metadata['parsed_files'][filename]
for subchild in _recursive_generator(file_ast, exclude, files_visited):
yield subchild
def _is_enabled(block_node, apache_vars):
"""Returns False if this block disables its children given loaded Apache data.
Checks to see whether this block_node is a conditional IfDefine or IfModule,
and returns what its argument evaluates to.
:param ApacheBlockNode block_node: block node to check.
:param dict apache_vars: dict that includes set of loaded modules and variables, under keys
"modules" and "defines", respectively.
"""
filters = {
"ifdefine": apache_vars["defines"],
"ifmodule": apache_vars["modules"]
}
if not block_node.name or block_node.name.lower() not in filters:
return True
loaded_set = filters[block_node.name.lower()]
name = block_node.parameters[0]
expect_loaded = not name.startswith("!")
name = name.lstrip("!")
loaded = (name in loaded_set)
return expect_loaded == loaded
class ApacheBlockNode(ApacheDirectiveNode):
""" apacheconfig implementation of BlockNode interface """
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.children: Tuple[ApacheParserNode, ...] = ()
super(ApacheBlockNode, self).__init__(**kwargs)
self._raw_children = self._raw
children = []
self.enabled = self.enabled and _is_enabled(self, self.metadata["apache_vars"])
for raw_node in self._raw_children:
node = None # type: Optional[ApacheParserNode]
metadata = self.metadata.copy()
metadata['ac_ast'] = raw_node
if raw_node.typestring == "comment":
node = ApacheCommentNode(comment=raw_node.name[2:],
metadata=metadata, ancestor=self,
filepath=self.filepath)
elif raw_node.typestring == "block":
parameters = util.parameters_from_string(raw_node.arguments)
node = ApacheBlockNode(name=raw_node.tag, parameters=parameters,
metadata=metadata, ancestor=self,
filepath=self.filepath, enabled=self.enabled)
else:
parameters = ()
if raw_node.value:
parameters = util.parameters_from_string(raw_node.value)
node = ApacheDirectiveNode(name=raw_node.name, parameters=parameters,
metadata=metadata, ancestor=self,
filepath=self.filepath, enabled=self.enabled)
children.append(node)
self.children = tuple(children)
def __eq__(self, other): # pragma: no cover
if isinstance(other, self.__class__):
@@ -98,8 +208,7 @@ class ApacheBlockNode(ApacheDirectiveNode):
self.metadata == other.metadata)
return False
# pylint: disable=unused-argument
def add_child_block(self, name, parameters=None, position=None): # pragma: no cover
def add_child_block(self, name, parameters=None, position=None): # pylint: disable=unused-argument
"""Adds a new BlockNode to the sequence of children"""
new_block = ApacheBlockNode(name=assertions.PASS,
parameters=assertions.PASS,
@@ -109,8 +218,7 @@ class ApacheBlockNode(ApacheDirectiveNode):
self.children += (new_block,)
return new_block
# pylint: disable=unused-argument
def add_child_directive(self, name, parameters=None, position=None): # pragma: no cover
def add_child_directive(self, name, parameters=None, position=None): # pylint: disable=unused-argument
"""Adds a new DirectiveNode to the sequence of children"""
new_dir = ApacheDirectiveNode(name=assertions.PASS,
parameters=assertions.PASS,
@@ -133,27 +241,27 @@ class ApacheBlockNode(ApacheDirectiveNode):
def find_blocks(self, name, exclude=True): # pylint: disable=unused-argument
"""Recursive search of BlockNodes from the sequence of children"""
return [ApacheBlockNode(name=assertions.PASS,
parameters=assertions.PASS,
ancestor=self,
filepath=assertions.PASS,
metadata=self.metadata)]
blocks = []
for child in _recursive_generator(self, exclude=exclude):
if isinstance(child, ApacheBlockNode) and child.name.lower() == name.lower():
blocks.append(child)
return blocks
def find_directives(self, name, exclude=True): # pylint: disable=unused-argument
"""Recursive search of DirectiveNodes from the sequence of children"""
return [ApacheDirectiveNode(name=assertions.PASS,
parameters=assertions.PASS,
ancestor=self,
filepath=assertions.PASS,
metadata=self.metadata)]
directives = []
for child in _recursive_generator(self, exclude=exclude):
if isinstance(child, ApacheDirectiveNode) and child.name.lower() == name.lower():
directives.append(child)
return directives
# pylint: disable=unused-argument
def find_comments(self, comment, exact=False): # pragma: no cover
def find_comments(self, comment): # pylint: disable=unused-argument
"""Recursive search of DirectiveNodes from the sequence of children"""
return [ApacheCommentNode(comment=assertions.PASS,
ancestor=self,
filepath=assertions.PASS,
metadata=self.metadata)]
comments = []
for child in _recursive_generator(self):
if isinstance(child, ApacheCommentNode) and comment in child.comment:
comments.append(child)
return comments
def delete_child(self, child): # pragma: no cover
"""Deletes a ParserNode from the sequence of children"""

View File

@@ -3,6 +3,7 @@ import fnmatch
from certbot_apache._internal import interfaces
PASS = "CERTBOT_PASS_ASSERT"
@@ -131,11 +132,11 @@ def assertEqualPathsList(first, second): # pragma: no cover
Checks that the two lists of file paths match. This assertion allows for wildcard
paths.
"""
if any(isPass(path) for path in first):
if any([isPass(path) for path in first]):
return
if any(isPass(path) for path in second):
if any([isPass(path) for path in second]):
return
for fpath in first:
assert any(fnmatch.fnmatch(fpath, spath) for spath in second)
assert any([fnmatch.fnmatch(fpath, spath) for spath in second])
for spath in second:
assert any(fnmatch.fnmatch(fpath, spath) for fpath in first)
assert any([fnmatch.fnmatch(fpath, spath) for fpath in first])

View File

@@ -6,7 +6,7 @@ Authors:
Raphael Pinson <raphink@gmail.com>
About: Reference
Online Apache configuration manual: https://httpd.apache.org/docs/trunk/
Online Apache configuration manual: http://httpd.apache.org/docs/trunk/
About: License
This file is licensed under the LGPL v2+.

View File

@@ -64,10 +64,10 @@ Translates over to:
"/files/etc/apache2/apache2.conf/bLoCk[1]",
]
"""
from typing import Set
from acme.magic_typing import Set # pylint: disable=unused-import, no-name-in-module
from certbot import errors
from certbot.compat import os
from certbot_apache._internal import apache_util
from certbot_apache._internal import assertions
from certbot_apache._internal import interfaces
@@ -80,7 +80,7 @@ class AugeasParserNode(interfaces.ParserNode):
def __init__(self, **kwargs):
ancestor, dirty, filepath, metadata = util.parsernode_kwargs(kwargs) # pylint: disable=unused-variable
super().__init__(**kwargs)
super(AugeasParserNode, self).__init__(**kwargs)
self.ancestor = ancestor
self.filepath = filepath
self.dirty = dirty
@@ -169,7 +169,7 @@ class AugeasCommentNode(AugeasParserNode):
def __init__(self, **kwargs):
comment, kwargs = util.commentnode_kwargs(kwargs) # pylint: disable=unused-variable
super().__init__(**kwargs)
super(AugeasCommentNode, self).__init__(**kwargs)
# self.comment = comment
self.comment = comment
@@ -188,7 +188,7 @@ class AugeasDirectiveNode(AugeasParserNode):
def __init__(self, **kwargs):
name, parameters, enabled, kwargs = util.directivenode_kwargs(kwargs)
super().__init__(**kwargs)
super(AugeasDirectiveNode, self).__init__(**kwargs)
self.name = name
self.enabled = enabled
if parameters:
@@ -245,7 +245,7 @@ class AugeasBlockNode(AugeasDirectiveNode):
""" Augeas implementation of BlockNode interface """
def __init__(self, **kwargs):
super().__init__(**kwargs)
super(AugeasBlockNode, self).__init__(**kwargs)
self.children = ()
def __eq__(self, other):
@@ -339,7 +339,7 @@ class AugeasBlockNode(AugeasDirectiveNode):
def find_blocks(self, name, exclude=True):
"""Recursive search of BlockNodes from the sequence of children"""
nodes = []
nodes = list()
paths = self._aug_find_blocks(name)
if exclude:
paths = self.parser.exclude_dirs(paths)
@@ -351,11 +351,11 @@ class AugeasBlockNode(AugeasDirectiveNode):
def find_directives(self, name, exclude=True):
"""Recursive search of DirectiveNodes from the sequence of children"""
nodes = []
nodes = list()
ownpath = self.metadata.get("augeaspath")
directives = self.parser.find_dir(name, start=ownpath, exclude=exclude)
already_parsed: Set[str] = set()
already_parsed = set() # type: Set[str]
for directive in directives:
# Remove the /arg part from the Augeas path
directive = directive.partition("/arg")[0]
@@ -374,7 +374,7 @@ class AugeasBlockNode(AugeasDirectiveNode):
:param str comment: Comment content to search for.
"""
nodes = []
nodes = list()
ownpath = self.metadata.get("augeaspath")
comments = self.parser.find_comments(comment, start=ownpath)
@@ -389,8 +389,8 @@ class AugeasBlockNode(AugeasDirectiveNode):
exception if it's unable to do so.
:param AugeasParserNode: child: A node to delete.
"""
if not self.parser.aug.remove(child.metadata["augeaspath"]):
if not self.parser.aug.remove(child.metadata["augeaspath"]):
raise errors.PluginError(
("Could not delete child node, the Augeas path: {} doesn't " +
"seem to exist.").format(child.metadata["augeaspath"])

View File

@@ -2,30 +2,34 @@
# pylint: disable=too-many-lines
from collections import defaultdict
import copy
from distutils.version import LooseVersion
import fnmatch
import logging
import re
import socket
import time
from typing import DefaultDict
from typing import Dict
from typing import List
from typing import Optional
from typing import Set
from typing import Union
import pkg_resources
import six
import zope.component
import zope.interface
try:
import apacheconfig
HAS_APACHECONFIG = True
except ImportError: # pragma: no cover
HAS_APACHECONFIG = False
from acme import challenges
from acme.magic_typing import DefaultDict
from acme.magic_typing import Dict
from acme.magic_typing import List
from acme.magic_typing import Set
from acme.magic_typing import Union
from certbot import errors
from certbot import interfaces
from certbot import util
from certbot.achallenges import KeyAuthorizationAnnotatedChallenge # pylint: disable=unused-import
from certbot.compat import filesystem
from certbot.compat import os
from certbot.display import util as display_util
from certbot.plugins import common
from certbot.plugins.enhancements import AutoHSTSEnhancement
from certbot.plugins.util import path_surgery
@@ -37,61 +41,10 @@ from certbot_apache._internal import dualparser
from certbot_apache._internal import http_01
from certbot_apache._internal import obj
from certbot_apache._internal import parser
from certbot_apache._internal.dualparser import DualBlockNode
from certbot_apache._internal.obj import VirtualHost
from certbot_apache._internal.parser import ApacheParser
try:
import apacheconfig
HAS_APACHECONFIG = True
except ImportError: # pragma: no cover
HAS_APACHECONFIG = False
logger = logging.getLogger(__name__)
class OsOptions:
"""
Dedicated class to describe the OS specificities (eg. paths, binary names)
that the Apache configurator needs to be aware to operate properly.
"""
def __init__(self,
server_root="/etc/apache2",
vhost_root="/etc/apache2/sites-available",
vhost_files="*",
logs_root="/var/log/apache2",
ctl="apache2ctl",
version_cmd: Optional[List[str]] = None,
restart_cmd: Optional[List[str]] = None,
restart_cmd_alt: Optional[List[str]] = None,
conftest_cmd: Optional[List[str]] = None,
enmod: Optional[str] = None,
dismod: Optional[str] = None,
le_vhost_ext="-le-ssl.conf",
handle_modules=False,
handle_sites=False,
challenge_location="/etc/apache2",
apache_bin: Optional[str] = None,
):
self.server_root = server_root
self.vhost_root = vhost_root
self.vhost_files = vhost_files
self.logs_root = logs_root
self.ctl = ctl
self.version_cmd = ['apache2ctl', '-v'] if not version_cmd else version_cmd
self.restart_cmd = ['apache2ctl', 'graceful'] if not restart_cmd else restart_cmd
self.restart_cmd_alt = restart_cmd_alt
self.conftest_cmd = ['apache2ctl', 'configtest'] if not conftest_cmd else conftest_cmd
self.enmod = enmod
self.dismod = dismod
self.le_vhost_ext = le_vhost_ext
self.handle_modules = handle_modules
self.handle_sites = handle_sites
self.challenge_location = challenge_location
self.bin = apache_bin
# TODO: Augeas sections ie. <VirtualHost>, <IfModule> beginning and closing
# tags need to be the same case, otherwise Augeas doesn't recognize them.
# This is not able to be completely remedied by regular expressions because
@@ -147,24 +100,28 @@ class ApacheConfigurator(common.Installer):
" change depending on the operating system Certbot is run on.)"
)
OS_DEFAULTS = OsOptions()
OS_DEFAULTS = dict(
server_root="/etc/apache2",
vhost_root="/etc/apache2/sites-available",
vhost_files="*",
logs_root="/var/log/apache2",
ctl="apache2ctl",
version_cmd=['apache2ctl', '-v'],
restart_cmd=['apache2ctl', 'graceful'],
conftest_cmd=['apache2ctl', 'configtest'],
enmod=None,
dismod=None,
le_vhost_ext="-le-ssl.conf",
handle_modules=False,
handle_sites=False,
challenge_location="/etc/apache2",
MOD_SSL_CONF_SRC=pkg_resources.resource_filename(
"certbot_apache", os.path.join("_internal", "options-ssl-apache.conf"))
)
def pick_apache_config(self, warn_on_no_mod_ssl=True):
"""
Pick the appropriate TLS Apache configuration file for current version of Apache and OS.
:param bool warn_on_no_mod_ssl: True if we should warn if mod_ssl is not found.
:return: the path to the TLS Apache configuration file to use
:rtype: str
"""
# Disabling TLS session tickets is supported by Apache 2.4.11+ and OpenSSL 1.0.2l+.
# So for old versions of Apache we pick a configuration without this option.
openssl_version = self.openssl_version(warn_on_no_mod_ssl)
if self.version < (2, 4, 11) or not openssl_version or\
LooseVersion(openssl_version) < LooseVersion('1.0.2l'):
return apache_util.find_ssl_apache_conf("old")
return apache_util.find_ssl_apache_conf("current")
def option(self, key):
"""Get a value from options"""
return self.options.get(key)
def _prepare_options(self):
"""
@@ -173,18 +130,18 @@ class ApacheConfigurator(common.Installer):
"""
opts = ["enmod", "dismod", "le_vhost_ext", "server_root", "vhost_root",
"logs_root", "challenge_location", "handle_modules", "handle_sites",
"ctl", "bin"]
"ctl"]
for o in opts:
# Config options use dashes instead of underscores
if self.conf(o.replace("_", "-")) is not None:
setattr(self.options, o, self.conf(o.replace("_", "-")))
self.options[o] = self.conf(o.replace("_", "-"))
else:
setattr(self.options, o, getattr(self.OS_DEFAULTS, o))
self.options[o] = self.OS_DEFAULTS[o]
# Special cases
self.options.version_cmd[0] = self.options.ctl
self.options.restart_cmd[0] = self.options.ctl
self.options.conftest_cmd[0] = self.options.ctl
self.options["version_cmd"][0] = self.option("ctl")
self.options["restart_cmd"][0] = self.option("ctl")
self.options["conftest_cmd"][0] = self.option("ctl")
@classmethod
def add_parser_arguments(cls, add):
@@ -199,31 +156,29 @@ class ApacheConfigurator(common.Installer):
else:
# cls.OS_DEFAULTS can be distribution specific, see override classes
DEFAULTS = cls.OS_DEFAULTS
add("enmod", default=DEFAULTS.enmod,
add("enmod", default=DEFAULTS["enmod"],
help="Path to the Apache 'a2enmod' binary")
add("dismod", default=DEFAULTS.dismod,
add("dismod", default=DEFAULTS["dismod"],
help="Path to the Apache 'a2dismod' binary")
add("le-vhost-ext", default=DEFAULTS.le_vhost_ext,
add("le-vhost-ext", default=DEFAULTS["le_vhost_ext"],
help="SSL vhost configuration extension")
add("server-root", default=DEFAULTS.server_root,
add("server-root", default=DEFAULTS["server_root"],
help="Apache server root directory")
add("vhost-root", default=None,
help="Apache server VirtualHost configuration root")
add("logs-root", default=DEFAULTS.logs_root,
add("logs-root", default=DEFAULTS["logs_root"],
help="Apache server logs directory")
add("challenge-location",
default=DEFAULTS.challenge_location,
default=DEFAULTS["challenge_location"],
help="Directory path for challenge configuration")
add("handle-modules", default=DEFAULTS.handle_modules,
add("handle-modules", default=DEFAULTS["handle_modules"],
help="Let installer handle enabling required modules for you " +
"(Only Ubuntu/Debian currently)")
add("handle-sites", default=DEFAULTS.handle_sites,
add("handle-sites", default=DEFAULTS["handle_sites"],
help="Let installer handle enabling sites for you " +
"(Only Ubuntu/Debian currently)")
add("ctl", default=DEFAULTS.ctl,
add("ctl", default=DEFAULTS["ctl"],
help="Full path to Apache control script")
add("bin", default=DEFAULTS.bin,
help="Full path to apache2/httpd binary")
def __init__(self, *args, **kwargs):
"""Initialize an Apache Configurator.
@@ -234,34 +189,32 @@ class ApacheConfigurator(common.Installer):
"""
version = kwargs.pop("version", None)
use_parsernode = kwargs.pop("use_parsernode", False)
openssl_version = kwargs.pop("openssl_version", None)
super().__init__(*args, **kwargs)
super(ApacheConfigurator, self).__init__(*args, **kwargs)
# Add name_server association dict
self.assoc: Dict[str, obj.VirtualHost] = {}
self.assoc = dict() # type: Dict[str, obj.VirtualHost]
# Outstanding challenges
self._chall_out: Set[KeyAuthorizationAnnotatedChallenge] = set()
self._chall_out = set() # type: Set[KeyAuthorizationAnnotatedChallenge]
# List of vhosts configured per wildcard domain on this run.
# used by deploy_cert() and enhance()
self._wildcard_vhosts: Dict[str, List[obj.VirtualHost]] = {}
self._wildcard_vhosts = dict() # type: Dict[str, List[obj.VirtualHost]]
# Maps enhancements to vhosts we've enabled the enhancement for
self._enhanced_vhosts: DefaultDict[str, Set[obj.VirtualHost]] = defaultdict(set)
self._enhanced_vhosts = defaultdict(set) # type: DefaultDict[str, Set[obj.VirtualHost]]
# Temporary state for AutoHSTS enhancement
self._autohsts: Dict[str, Dict[str, Union[int, float]]] = {}
self._autohsts = {} # type: Dict[str, Dict[str, Union[int, float]]]
# Reverter save notes
self.save_notes = ""
# Should we use ParserNode implementation instead of the old behavior
self.USE_PARSERNODE = use_parsernode
# Saves the list of file paths that were parsed initially, and
# not added to parser tree by self.conf("vhost-root") for example.
self.parsed_paths: List[str] = []
self.parsed_paths = [] # type: List[str]
# These will be set in the prepare function
self._prepared = False
self.parser: ApacheParser
self.parser_root: Optional[DualBlockNode] = None
self.parser = None
self.parser_root = None
self.version = version
self._openssl_version = openssl_version
self.vhosts: List[VirtualHost]
self.vhosts = None
self.options = copy.deepcopy(self.OS_DEFAULTS)
self._enhance_func = {"redirect": self._enable_redirect,
"ensure-http-header": self._set_http_header,
@@ -277,59 +230,6 @@ class ApacheConfigurator(common.Installer):
"""Full absolute path to digest of updated SSL configuration file."""
return os.path.join(self.config.config_dir, constants.UPDATED_MOD_SSL_CONF_DIGEST)
def _open_module_file(self, ssl_module_location):
"""Extract the open lines of openssl_version for testing purposes"""
try:
with open(ssl_module_location, mode="rb") as f:
contents = f.read()
except IOError as error:
logger.debug(str(error), exc_info=True)
return None
return contents
def openssl_version(self, warn_on_no_mod_ssl=True):
"""Lazily retrieve openssl version
:param bool warn_on_no_mod_ssl: `True` if we should warn if mod_ssl is not found. Set to
`False` when we know we'll try to enable mod_ssl later. This is currently debian/ubuntu,
when called from `prepare`.
:return: the OpenSSL version as a string, or None.
:rtype: str or None
"""
if self._openssl_version:
return self._openssl_version
# Step 1. Determine the location of ssl_module
try:
ssl_module_location = self.parser.modules['ssl_module']
except KeyError:
if warn_on_no_mod_ssl:
logger.warning("Could not find ssl_module; not disabling session tickets.")
return None
if ssl_module_location:
# Possibility A: ssl_module is a DSO
ssl_module_location = self.parser.standard_path_from_server_root(ssl_module_location)
else:
# Possibility B: ssl_module is statically linked into Apache
if self.options.bin:
ssl_module_location = self.options.bin
else:
logger.warning("ssl_module is statically linked but --apache-bin is "
"missing; not disabling session tickets.")
return None
# Step 2. Grep in the binary for openssl version
contents = self._open_module_file(ssl_module_location)
if not contents:
logger.warning("Unable to read ssl_module file; not disabling session tickets.")
return None
# looks like: OpenSSL 1.0.2s 28 May 2019
matches = re.findall(br"OpenSSL ([0-9]\.[^ ]+) ", contents)
if not matches:
logger.warning("Could not find OpenSSL version; not disabling session tickets.")
return None
self._openssl_version = matches[0].decode('UTF-8')
return self._openssl_version
def prepare(self):
"""Prepare the authenticator/installer.
@@ -342,7 +242,7 @@ class ApacheConfigurator(common.Installer):
self._prepare_options()
# Verify Apache is installed
self._verify_exe_availability(self.options.ctl)
self._verify_exe_availability(self.option("ctl"))
# Make sure configuration is valid
self.config_test()
@@ -355,9 +255,6 @@ class ApacheConfigurator(common.Installer):
if self.version < (2, 2):
raise errors.NotSupportedError(
"Apache Version {0} not supported.".format(str(self.version)))
elif self.version < (2, 4):
logger.warning('Support for Apache 2.2 is deprecated and will be removed in a '
'future release.')
# Recover from previous crash before Augeas initialization to have the
# correct parse tree from the get go.
@@ -368,11 +265,11 @@ class ApacheConfigurator(common.Installer):
# Set up ParserNode root
pn_meta = {"augeasparser": self.parser,
"augeaspath": self.parser.get_root_augpath(),
"serverroot": self.option("server_root"),
"ac_ast": None}
if self.USE_PARSERNODE:
parser_root = self.get_parsernode_root(pn_meta)
self.parser_root = parser_root
self.parsed_paths = parser_root.parsed_paths()
if self.USE_PARSERNODE and HAS_APACHECONFIG:
self.parser_root = self.get_parsernode_root(pn_meta)
self.parsed_paths = self.parser_root.parsed_paths()
# Check for errors in parsing files with Augeas
self.parser.check_parsing_errors("httpd.aug")
@@ -380,22 +277,18 @@ class ApacheConfigurator(common.Installer):
# Get all of the available vhosts
self.vhosts = self.get_virtual_hosts()
# We may try to enable mod_ssl later. If so, we shouldn't warn if we can't find it now.
# This is currently only true for debian/ubuntu.
warn_on_no_mod_ssl = not self.options.handle_modules
self.install_ssl_options_conf(self.mod_ssl_conf,
self.updated_mod_ssl_conf_digest,
warn_on_no_mod_ssl)
self.updated_mod_ssl_conf_digest)
# Prevent two Apache plugins from modifying a config at once
try:
util.lock_dir_until_exit(self.options.server_root)
util.lock_dir_until_exit(self.option("server_root"))
except (OSError, errors.LockError):
logger.debug("Encountered error:", exc_info=True)
raise errors.PluginError(
"Unable to create a lock file in {0}. Are you running"
" Certbot with sufficient privileges to modify your"
" Apache configuration?".format(self.options.server_root))
" Apache configuration?".format(self.option("server_root")))
self._prepared = True
def save(self, title=None, temporary=False):
@@ -431,10 +324,10 @@ class ApacheConfigurator(common.Installer):
:raises .errors.PluginError: If unable to recover the configuration
"""
super().recovery_routine()
super(ApacheConfigurator, self).recovery_routine()
# Reload configuration after these changes take effect if needed
# ie. ApacheParser has been initialized.
if hasattr(self, "parser"):
if self.parser:
# TODO: wrap into non-implementation specific parser interface
self.parser.aug.load()
@@ -456,7 +349,7 @@ class ApacheConfigurator(common.Installer):
the function is unable to correctly revert the configuration
"""
super().rollback_checkpoints(rollback)
super(ApacheConfigurator, self).rollback_checkpoints(rollback)
self.parser.aug.load()
def _verify_exe_availability(self, exe):
@@ -470,30 +363,46 @@ class ApacheConfigurator(common.Installer):
"""Initializes the ApacheParser"""
# If user provided vhost_root value in command line, use it
return parser.ApacheParser(
self.options.server_root, self.conf("vhost-root"),
self.option("server_root"), self.conf("vhost-root"),
self.version, configurator=self)
def get_parsernode_root(self, metadata):
"""Initializes the ParserNode parser root instance."""
if HAS_APACHECONFIG:
apache_vars = {}
apache_vars["defines"] = apache_util.parse_defines(self.options.ctl)
apache_vars["includes"] = apache_util.parse_includes(self.options.ctl)
apache_vars["modules"] = apache_util.parse_modules(self.options.ctl)
metadata["apache_vars"] = apache_vars
apache_vars = dict()
apache_vars["defines"] = set(apache_util.parse_defines(self.option("ctl")))
apache_vars["includes"] = set(apache_util.parse_includes(self.option("ctl")))
apache_vars["modules"] = set(apache_util.parse_modules(self.option("ctl")))
metadata["apache_vars"] = apache_vars
with apacheconfig.make_loader(writable=True,
**apacheconfig.flavors.NATIVE_APACHE) as loader:
with open(self.parser.loc["root"]) as f:
with apacheconfig.make_loader(writable=True,
**apacheconfig.flavors.NATIVE_APACHE) as loader:
metadata["ac_ast"] = loader.loads(f.read())
metadata["ac_ast"] = loader.loads(f.read())
metadata["loader"] = loader
metadata["parsed_files"] = {}
return dualparser.DualBlockNode(
name=assertions.PASS,
ancestor=None,
filepath=self.parser.loc["root"],
metadata=metadata
)
return dualparser.DualBlockNode(
name=assertions.PASS,
ancestor=None,
filepath=self.parser.loc["root"],
metadata=metadata
)
def _wildcard_domain(self, domain):
"""
Checks if domain is a wildcard domain
:param str domain: Domain to check
:returns: If the domain is wildcard domain
:rtype: bool
"""
if isinstance(domain, six.text_type):
wildcard_marker = u"*."
else:
wildcard_marker = b"*."
return domain.startswith(wildcard_marker)
def deploy_cert(self, domain, cert_path, key_path,
chain_path=None, fullchain_path=None):
@@ -516,8 +425,6 @@ class ApacheConfigurator(common.Installer):
vhosts = self.choose_vhosts(domain)
for vhost in vhosts:
self._deploy_cert(vhost, cert_path, key_path, chain_path, fullchain_path)
display_util.notify("Successfully deployed certificate for {} to {}"
.format(domain, vhost.filep))
def choose_vhosts(self, domain, create_if_no_ssl=True):
"""
@@ -531,7 +438,7 @@ class ApacheConfigurator(common.Installer):
:rtype: `list` of :class:`~certbot_apache._internal.obj.VirtualHost`
"""
if util.is_wildcard_domain(domain):
if self._wildcard_domain(domain):
if domain in self._wildcard_vhosts:
# Vhosts for a wildcard domain were already selected
return self._wildcard_vhosts[domain]
@@ -556,19 +463,6 @@ class ApacheConfigurator(common.Installer):
return list(matched)
def _raise_no_suitable_vhost_error(self, target_name: str):
"""
Notifies the user that Certbot could not find a vhost to secure
and raises an error.
:param str target_name: The server name that could not be mapped
:raises errors.PluginError: Raised unconditionally
"""
raise errors.PluginError(
"Certbot could not find a VirtualHost for {0} in the Apache "
"configuration. Please create a VirtualHost with a ServerName "
"matching {0} and try again.".format(target_name)
)
def _in_wildcard_scope(self, name, domain):
"""
Helper method for _vhosts_for_wildcard() that makes sure that the domain
@@ -589,7 +483,7 @@ class ApacheConfigurator(common.Installer):
# Go through the vhosts, making sure that we cover all the names
# present, but preferring the SSL vhosts
filtered_vhosts = {}
filtered_vhosts = dict()
for vhost in vhosts:
for name in vhost.get_names():
if vhost.ssl:
@@ -606,11 +500,16 @@ class ApacheConfigurator(common.Installer):
dialog_output = display_ops.select_vhost_multiple(list(dialog_input))
if not dialog_output:
self._raise_no_suitable_vhost_error(domain)
logger.error(
"No vhost exists with servername or alias for domain %s. "
"No vhost was selected. Please specify ServerName or ServerAlias "
"in the Apache config.",
domain)
raise errors.PluginError("No vhost selected")
# Make sure we create SSL vhosts for the ones that are HTTP only
# if requested.
return_vhosts = []
return_vhosts = list()
for vhost in dialog_output:
if not vhost.ssl:
return_vhosts.append(self.make_vhost_ssl(vhost))
@@ -631,11 +530,6 @@ class ApacheConfigurator(common.Installer):
# cert_key... can all be parsed appropriately
self.prepare_server_https("443")
# If we haven't managed to enable mod_ssl by this point, error out
if "ssl_module" not in self.parser.modules:
raise errors.MisconfigurationError("Could not find ssl_module; "
"not installing certificate.")
# Add directives and remove duplicates
self._add_dummy_ssl_directives(vhost.path)
self._clean_vhost(vhost)
@@ -650,6 +544,21 @@ class ApacheConfigurator(common.Installer):
path["chain_path"] = self.parser.find_dir(
"SSLCertificateChainFile", None, vhost.path)
# Handle errors when certificate/key directives cannot be found
if not path["cert_path"]:
logger.warning(
"Cannot find an SSLCertificateFile directive in %s. "
"VirtualHost was not modified", vhost.path)
raise errors.PluginError(
"Unable to find an SSLCertificateFile directive")
elif not path["cert_key"]:
logger.warning(
"Cannot find an SSLCertificateKeyFile directive for "
"certificate in %s. VirtualHost was not modified", vhost.path)
raise errors.PluginError(
"Unable to find an SSLCertificateKeyFile directive for "
"certificate")
logger.info("Deploying Certificate to VirtualHost %s", vhost.filep)
if self.version < (2, 4, 8) or (chain_path and not fullchain_path):
@@ -730,7 +639,12 @@ class ApacheConfigurator(common.Installer):
# Select a vhost from a list
vhost = display_ops.select_vhost(target_name, self.vhosts)
if vhost is None:
self._raise_no_suitable_vhost_error(target_name)
logger.error(
"No vhost exists with servername or alias of %s. "
"No vhost was selected. Please specify ServerName or ServerAlias "
"in the Apache config.",
target_name)
raise errors.PluginError("No vhost selected")
if temp:
return vhost
if not vhost.ssl:
@@ -866,7 +780,7 @@ class ApacheConfigurator(common.Installer):
:rtype: set
"""
all_names: Set[str] = set()
all_names = set() # type: Set[str]
vhost_macro = []
@@ -1030,8 +944,8 @@ class ApacheConfigurator(common.Installer):
"""
# Search base config, and all included paths for VirtualHosts
file_paths: Dict[str, str] = {}
internal_paths: DefaultDict[str, Set[str]] = defaultdict(set)
file_paths = {} # type: Dict[str, str]
internal_paths = defaultdict(set) # type: DefaultDict[str, Set[str]]
vhs = []
# Make a list of parser paths because the parser_paths
# dictionary may be modified during the loop.
@@ -1082,9 +996,6 @@ class ApacheConfigurator(common.Installer):
:rtype: list
"""
if not self.parser_root:
raise errors.Error("This ApacheConfigurator instance is not" # pragma: no cover
" configured to use a node parser.")
vhs = []
vhosts = self.parser_root.find_blocks("VirtualHost", exclude=False)
for vhblock in vhosts:
@@ -1337,27 +1248,19 @@ class ApacheConfigurator(common.Installer):
:param boolean temp: If the change is temporary
"""
if self.options.handle_modules:
if self.option("handle_modules"):
if self.version >= (2, 4) and ("socache_shmcb_module" not in
self.parser.modules):
self.enable_mod("socache_shmcb", temp=temp)
if "ssl_module" not in self.parser.modules:
self.enable_mod("ssl", temp=temp)
# Make sure we're not throwing away any unwritten changes to the config
self.parser.ensure_augeas_state()
self.parser.aug.load()
self.parser.reset_modules() # Reset to load the new ssl_module path
# Call again because now we can gate on openssl version
self.install_ssl_options_conf(self.mod_ssl_conf,
self.updated_mod_ssl_conf_digest,
warn_on_no_mod_ssl=True)
def make_vhost_ssl(self, nonssl_vhost):
"""Makes an ssl_vhost version of a nonssl_vhost.
Duplicates vhost and adds default ssl options
New vhost will reside as (nonssl_vhost.path) +
``self.options.le_vhost_ext``
``self.option("le_vhost_ext")``
.. note:: This function saves the configuration
@@ -1456,15 +1359,15 @@ class ApacheConfigurator(common.Installer):
"""
if self.conf("vhost-root") and os.path.exists(self.conf("vhost-root")):
fp = os.path.join(filesystem.realpath(self.options.vhost_root),
fp = os.path.join(filesystem.realpath(self.option("vhost_root")),
os.path.basename(non_ssl_vh_fp))
else:
# Use non-ssl filepath
fp = filesystem.realpath(non_ssl_vh_fp)
if fp.endswith(".conf"):
return fp[:-(len(".conf"))] + self.options.le_vhost_ext
return fp + self.options.le_vhost_ext
return fp[:-(len(".conf"))] + self.option("le_vhost_ext")
return fp + self.option("le_vhost_ext")
def _sift_rewrite_rule(self, line):
"""Decides whether a line should be copied to a SSL vhost.
@@ -1486,7 +1389,7 @@ class ApacheConfigurator(common.Installer):
if not line.lower().lstrip().startswith("rewriterule"):
return False
# According to: https://httpd.apache.org/docs/2.4/rewrite/flags.html
# According to: http://httpd.apache.org/docs/2.4/rewrite/flags.html
# The syntax of a RewriteRule is:
# RewriteRule pattern target [Flag1,Flag2,Flag3]
# i.e. target is required, so it must exist.
@@ -1538,11 +1441,12 @@ class ApacheConfigurator(common.Installer):
raise errors.PluginError("Unable to write/read in make_vhost_ssl")
if sift:
display_util.notify(
f"Some rewrite rules copied from {vhost.filep} were disabled in the "
f"vhost for your HTTPS site located at {ssl_fp} because they have "
"the potential to create redirection loops."
)
reporter = zope.component.getUtility(interfaces.IReporter)
reporter.add_message(
"Some rewrite rules copied from {0} were disabled in the "
"vhost for your HTTPS site located at {1} because they have "
"the potential to create redirection loops.".format(
vhost.filep, ssl_fp), reporter.MEDIUM_PRIORITY)
self.parser.aug.set("/augeas/files%s/mtime" % (self._escape(ssl_fp)), "0")
self.parser.aug.set("/augeas/files%s/mtime" % (self._escape(vhost.filep)), "0")
@@ -1602,7 +1506,7 @@ class ApacheConfigurator(common.Installer):
result.append(comment)
sift = True
result.append('\n'.join('# ' + l for l in chunk))
result.append('\n'.join(['# ' + l for l in chunk]))
else:
result.append('\n'.join(chunk))
return result, sift
@@ -1742,7 +1646,7 @@ class ApacheConfigurator(common.Installer):
for addr in vhost.addrs:
# In Apache 2.2, when a NameVirtualHost directive is not
# set, "*" and "_default_" will conflict when sharing a port
addrs = {addr,}
addrs = set((addr,))
if addr.get_addr() in ("*", "_default_"):
addrs.update(obj.Addr((a, addr.get_port(),))
for a in ("*", "_default_"))
@@ -1871,13 +1775,13 @@ class ApacheConfigurator(common.Installer):
if options:
msg_enhancement += ": " + options
msg = msg_tmpl.format(domain, msg_enhancement)
logger.error(msg)
logger.warning(msg)
raise errors.PluginError(msg)
try:
for vhost in vhosts:
func(vhost, options)
except errors.PluginError:
logger.error("Failed %s for %s", enhancement, domain)
logger.warning("Failed %s for %s", enhancement, domain)
raise
def _autohsts_increase(self, vhost, id_str, nextstep):
@@ -1933,7 +1837,7 @@ class ApacheConfigurator(common.Installer):
try:
self._autohsts = self.storage.fetch("autohsts")
except KeyError:
self._autohsts = {}
self._autohsts = dict()
def _autohsts_save_state(self):
"""
@@ -2192,7 +2096,7 @@ class ApacheConfigurator(common.Installer):
# There can be other RewriteRule directive lines in vhost config.
# rewrite_args_dict keys are directive ids and the corresponding value
# for each is a list of arguments to that directive.
rewrite_args_dict: DefaultDict[str, List[str]] = defaultdict(list)
rewrite_args_dict = defaultdict(list) # type: DefaultDict[str, List[str]]
pat = r'(.*directive\[\d+\]).*'
for match in rewrite_path:
m = re.match(pat, match)
@@ -2286,7 +2190,7 @@ class ApacheConfigurator(common.Installer):
if ssl_vhost.aliases:
serveralias = "ServerAlias " + " ".join(ssl_vhost.aliases)
rewrite_rule_args: List[str] = []
rewrite_rule_args = [] # type: List[str]
if self.get_version() >= (2, 3, 9):
rewrite_rule_args = constants.REWRITE_HTTPS_ARGS_WITH_END
else:
@@ -2307,7 +2211,7 @@ class ApacheConfigurator(common.Installer):
addr in self._get_proposed_addrs(ssl_vhost)),
servername, serveralias,
" ".join(rewrite_rule_args),
self.options.logs_root))
self.option("logs_root")))
def _write_out_redirect(self, ssl_vhost, text):
# This is the default name
@@ -2319,7 +2223,7 @@ class ApacheConfigurator(common.Installer):
if len(ssl_vhost.name) < (255 - (len(redirect_filename) + 1)):
redirect_filename = "le-redirect-%s.conf" % ssl_vhost.name
redirect_filepath = os.path.join(self.options.vhost_root,
redirect_filepath = os.path.join(self.option("vhost_root"),
redirect_filename)
# Register the new file that will be created
@@ -2401,7 +2305,7 @@ class ApacheConfigurator(common.Installer):
vhost.enabled = True
return
def enable_mod(self, mod_name, temp=False): # pylint: disable=unused-argument
def enable_mod(self, mod_name, temp=False):
"""Enables module in Apache.
Both enables and reloads Apache so module is active.
@@ -2439,18 +2343,19 @@ class ApacheConfigurator(common.Installer):
"""
try:
util.run_script(self.options.restart_cmd)
util.run_script(self.option("restart_cmd"))
except errors.SubprocessError as err:
logger.warning("Unable to restart apache using %s",
self.options.restart_cmd)
alt_restart = self.options.restart_cmd_alt
logger.info("Unable to restart apache using %s",
self.option("restart_cmd"))
alt_restart = self.option("restart_cmd_alt")
if alt_restart:
logger.debug("Trying alternative restart command: %s",
alt_restart)
# There is an alternative restart command available
# This usually is "restart" verb while original is "graceful"
try:
util.run_script(self.options.restart_cmd_alt)
util.run_script(self.option(
"restart_cmd_alt"))
return
except errors.SubprocessError as secerr:
error = str(secerr)
@@ -2465,7 +2370,7 @@ class ApacheConfigurator(common.Installer):
"""
try:
util.run_script(self.options.conftest_cmd)
util.run_script(self.option("conftest_cmd"))
except errors.SubprocessError as err:
raise errors.MisconfigurationError(str(err))
@@ -2481,11 +2386,11 @@ class ApacheConfigurator(common.Installer):
"""
try:
stdout, _ = util.run_script(self.options.version_cmd)
stdout, _ = util.run_script(self.option("version_cmd"))
except errors.SubprocessError:
raise errors.PluginError(
"Unable to run %s -v" %
self.options.version_cmd)
self.option("version_cmd"))
regex = re.compile(r"Apache/([0-9\.]*)", re.IGNORECASE)
matches = regex.findall(stdout)
@@ -2493,7 +2398,7 @@ class ApacheConfigurator(common.Installer):
if len(matches) != 1:
raise errors.PluginError("Unable to find Apache version")
return tuple(int(i) for i in matches[0].split("."))
return tuple([int(i) for i in matches[0].split(".")])
def more_info(self):
"""Human-readable string to help understand the module"""
@@ -2505,11 +2410,6 @@ class ApacheConfigurator(common.Installer):
version=".".join(str(i) for i in self.version))
)
def auth_hint(self, failed_achalls): # pragma: no cover
return ("The Certificate Authority failed to verify the temporary Apache configuration "
"changes made by Certbot. Ensure that the listed domains point to this Apache "
"server and that it is accessible from the internet.")
###########################################################################
# Challenges Section
###########################################################################
@@ -2567,19 +2467,14 @@ class ApacheConfigurator(common.Installer):
self.restart()
self.parser.reset_modules()
def install_ssl_options_conf(self, options_ssl, options_ssl_digest, warn_on_no_mod_ssl=True):
"""Copy Certbot's SSL options file into the system's config dir if required.
:param bool warn_on_no_mod_ssl: True if we should warn if mod_ssl is not found.
"""
def install_ssl_options_conf(self, options_ssl, options_ssl_digest):
"""Copy Certbot's SSL options file into the system's config dir if required."""
# XXX if we ever try to enforce a local privilege boundary (eg, running
# certbot for unprivileged users via setuid), this function will need
# to be modified.
apache_config_path = self.pick_apache_config(warn_on_no_mod_ssl)
return common.install_version_controlled_file(
options_ssl, options_ssl_digest, apache_config_path, constants.ALL_SSL_OPTIONS_HASHES)
return common.install_version_controlled_file(options_ssl, options_ssl_digest,
self.option("MOD_SSL_CONF_SRC"), constants.ALL_SSL_OPTIONS_HASHES)
def enable_autohsts(self, _unused_lineage, domains):
"""
@@ -2603,7 +2498,7 @@ class ApacheConfigurator(common.Installer):
msg_tmpl = ("Certbot was not able to find SSL VirtualHost for a "
"domain {0} for enabling AutoHSTS enhancement.")
msg = msg_tmpl.format(d)
logger.error(msg)
logger.warning(msg)
raise errors.PluginError(msg)
for vh in vhosts:
try:
@@ -2689,7 +2584,7 @@ class ApacheConfigurator(common.Installer):
except errors.PluginError:
msg = ("Could not find VirtualHost with ID {0}, disabling "
"AutoHSTS for this VirtualHost").format(id_str)
logger.error(msg)
logger.warning(msg)
# Remove the orphaned AutoHSTS entry from pluginstorage
self._autohsts.pop(id_str)
continue
@@ -2729,7 +2624,7 @@ class ApacheConfigurator(common.Installer):
except errors.PluginError:
msg = ("VirtualHost with id {} was not found, unable to "
"make HSTS max-age permanent.").format(id_str)
logger.error(msg)
logger.warning(msg)
self._autohsts.pop(id_str)
continue
if self._autohsts_vhost_in_lineage(vhost, lineage):

View File

@@ -26,7 +26,6 @@ ALL_SSL_OPTIONS_HASHES = [
'06675349e457eae856120cdebb564efe546f0b87399f2264baeb41e442c724c7',
'5cc003edd93fb9cd03d40c7686495f8f058f485f75b5e764b789245a386e6daf',
'007cd497a56a3bb8b6a2c1aeb4997789e7e38992f74e44cc5d13a625a738ac73',
'34783b9e2210f5c4a23bced2dfd7ec289834716673354ed7c7abf69fe30192a3',
]
"""SHA256 hashes of the contents of previous versions of all versions of MOD_SSL_CONF_SRC"""

View File

@@ -21,7 +21,7 @@ def select_vhost_multiple(vhosts):
:rtype: :class:`list`of type `~obj.Vhost`
"""
if not vhosts:
return []
return list()
tags_list = [vhost.display_repr()+"\n" for vhost in vhosts]
# Remove the extra newline from the last entry
if tags_list:
@@ -37,7 +37,7 @@ def select_vhost_multiple(vhosts):
def _reversemap_vhosts(names, vhosts):
"""Helper function for select_vhost_multiple for mapping string
representations back to actual vhost objects"""
return_vhosts = []
return_vhosts = list()
for selection in names:
for vhost in vhosts:
@@ -119,7 +119,7 @@ def _vhost_menu(domain, vhosts):
"guidance in non-interactive mode. Certbot may need "
"vhosts to be explicitly labelled with ServerName or "
"ServerAlias directives.".format(domain))
logger.error(msg)
logger.warning(msg)
raise errors.MissingCommandlineFlag(msg)
return code, tag

View File

@@ -1,10 +1,10 @@
""" Dual ParserNode implementation """
from certbot_apache._internal import apacheparser
from certbot_apache._internal import assertions
from certbot_apache._internal import augeasparser
from certbot_apache._internal import apacheparser
class DualNodeBase:
class DualNodeBase(object):
""" Dual parser interface for in development testing. This is used as the
base class for dual parser interface classes. This class handles runtime
attribute value assertions."""
@@ -49,7 +49,7 @@ class DualNodeBase:
pass_primary = assertions.isPassNodeList(primary_res)
pass_secondary = assertions.isPassNodeList(secondary_res)
new_nodes = []
new_nodes = list()
if pass_primary and pass_secondary:
# Both unimplemented
@@ -221,7 +221,7 @@ class DualBlockNode(DualNodeBase):
implementations to a list of tuples.
"""
matched = []
matched = list()
for p in primary_list:
match = None
for s in secondary_list:

View File

@@ -1,4 +1,6 @@
""" Entry point for Apache Plugin """
# Pylint does not like disutils.version when running inside a venv.
# See: https://github.com/PyCQA/pylint/issues/73
from distutils.version import LooseVersion
from certbot import util

View File

@@ -1,9 +1,8 @@
"""A class that performs HTTP-01 challenges for Apache"""
import errno
import logging
from typing import List
from typing import Set
from acme.magic_typing import List
from acme.magic_typing import Set
from certbot import errors
from certbot.compat import filesystem
from certbot.compat import os
@@ -47,7 +46,7 @@ class ApacheHttp01(common.ChallengePerformer):
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
super(ApacheHttp01, self).__init__(*args, **kwargs)
self.challenge_conf_pre = os.path.join(
self.configurator.conf("challenge-location"),
"le_http_01_challenge_pre.conf")
@@ -57,7 +56,7 @@ class ApacheHttp01(common.ChallengePerformer):
self.challenge_dir = os.path.join(
self.configurator.config.work_dir,
"http_challenges")
self.moded_vhosts: Set[VirtualHost] = set()
self.moded_vhosts = set() # type: Set[VirtualHost]
def perform(self):
"""Perform all HTTP-01 challenges."""
@@ -93,7 +92,7 @@ class ApacheHttp01(common.ChallengePerformer):
self.configurator.enable_mod(mod, temp=True)
def _mod_config(self):
selected_vhosts: List[VirtualHost] = []
selected_vhosts = [] # type: List[VirtualHost]
http_port = str(self.configurator.config.http01_port)
for chall in self.achalls:
# Search for matching VirtualHosts
@@ -169,15 +168,7 @@ class ApacheHttp01(common.ChallengePerformer):
def _set_up_challenges(self):
if not os.path.isdir(self.challenge_dir):
old_umask = filesystem.umask(0o022)
try:
filesystem.makedirs(self.challenge_dir, 0o755)
except OSError as exception:
if exception.errno not in (errno.EEXIST, errno.EISDIR):
raise errors.PluginError(
"Couldn't create root for http-01 challenge")
finally:
filesystem.umask(old_umask)
filesystem.makedirs(self.challenge_dir, 0o755)
responses = []
for achall in self.achalls:

View File

@@ -100,9 +100,13 @@ For this reason the internal representation of data should not ignore the case.
"""
import abc
import six
from acme.magic_typing import Any, Dict, Optional, Tuple # pylint: disable=unused-import, no-name-in-module
class ParserNode(object, metaclass=abc.ABCMeta):
@six.add_metaclass(abc.ABCMeta)
class ParserNode(object):
"""
ParserNode is the basic building block of the tree of such nodes,
representing the structure of the configuration. It is largely meant to keep
@@ -201,7 +205,9 @@ class ParserNode(object, metaclass=abc.ABCMeta):
"""
class CommentNode(ParserNode, metaclass=abc.ABCMeta):
# Linter rule exclusion done because of https://github.com/PyCQA/pylint/issues/179
@six.add_metaclass(abc.ABCMeta) # pylint: disable=abstract-method
class CommentNode(ParserNode):
"""
CommentNode class is used for representation of comments within the parsed
configuration structure. Because of the nature of comments, it is not able
@@ -238,13 +244,14 @@ class CommentNode(ParserNode, metaclass=abc.ABCMeta):
created or changed after the last save. Default: False.
:type dirty: bool
"""
super().__init__(ancestor=kwargs['ancestor'],
super(CommentNode, self).__init__(ancestor=kwargs['ancestor'],
dirty=kwargs.get('dirty', False),
filepath=kwargs['filepath'],
metadata=kwargs.get('metadata', {})) # pragma: no cover
class DirectiveNode(ParserNode, metaclass=abc.ABCMeta):
@six.add_metaclass(abc.ABCMeta)
class DirectiveNode(ParserNode):
"""
DirectiveNode class represents a configuration directive within the configuration.
It can have zero or more parameters attached to it. Because of the nature of
@@ -302,7 +309,7 @@ class DirectiveNode(ParserNode, metaclass=abc.ABCMeta):
:type enabled: bool
"""
super().__init__(ancestor=kwargs['ancestor'],
super(DirectiveNode, self).__init__(ancestor=kwargs['ancestor'],
dirty=kwargs.get('dirty', False),
filepath=kwargs['filepath'],
metadata=kwargs.get('metadata', {})) # pragma: no cover
@@ -319,7 +326,8 @@ class DirectiveNode(ParserNode, metaclass=abc.ABCMeta):
"""
class BlockNode(DirectiveNode, metaclass=abc.ABCMeta):
@six.add_metaclass(abc.ABCMeta)
class BlockNode(DirectiveNode):
"""
BlockNode class represents a block of nested configuration directives, comments
and other blocks as its children. A BlockNode can have zero or more parameters

View File

@@ -1,7 +1,7 @@
"""Module contains classes used by the Apache Configurator."""
import re
from typing import Set
from acme.magic_typing import Set
from certbot.plugins import common
@@ -20,13 +20,16 @@ class Addr(common.Addr):
self.is_wildcard() and other.is_wildcard()))
return False
def __ne__(self, other):
return not self.__eq__(other)
def __repr__(self):
return "certbot_apache._internal.obj.Addr(" + repr(self.tup) + ")"
def __hash__(self): # pylint: disable=useless-super-delegation
# Python 3 requires explicit overridden for __hash__ if __eq__ or
# __cmp__ is overridden. See https://bugs.python.org/issue2235
return super().__hash__()
return super(Addr, self).__hash__()
def _addr_less_specific(self, addr):
"""Returns if addr.get_addr() is more specific than self.get_addr()."""
@@ -95,7 +98,7 @@ class Addr(common.Addr):
return self.get_addr_obj(port)
class VirtualHost:
class VirtualHost(object):
"""Represents an Apache Virtualhost.
:ivar str filep: file path of VH
@@ -137,7 +140,7 @@ class VirtualHost:
def get_names(self):
"""Return a set of all names."""
all_names: Set[str] = set()
all_names = set() # type: Set[str]
all_names.update(self.aliases)
# Strip out any scheme:// and <port> field from servername
if self.name is not None:
@@ -188,6 +191,9 @@ class VirtualHost:
return False
def __ne__(self, other):
return not self.__eq__(other)
def __hash__(self):
return hash((self.filep, self.path,
tuple(self.addrs), tuple(self.get_names()),
@@ -245,7 +251,7 @@ class VirtualHost:
# already_found acts to keep everything very conservative.
# Don't allow multiple ip:ports in same set.
already_found: Set[str] = set()
already_found = set() # type: Set[str]
for addr in vhost.addrs:
for local_addr in self.addrs:

View File

@@ -1,16 +1,17 @@
""" Distribution specific override class for Arch Linux """
import pkg_resources
import zope.interface
from certbot import interfaces
from certbot.compat import os
from certbot_apache._internal import configurator
from certbot_apache._internal.configurator import OsOptions
@zope.interface.provider(interfaces.IPluginFactory)
class ArchConfigurator(configurator.ApacheConfigurator):
"""Arch Linux specific ApacheConfigurator override class"""
OS_DEFAULTS = OsOptions(
OS_DEFAULTS = dict(
server_root="/etc/httpd",
vhost_root="/etc/httpd/conf",
vhost_files="*.conf",
@@ -19,5 +20,12 @@ class ArchConfigurator(configurator.ApacheConfigurator):
version_cmd=['apachectl', '-v'],
restart_cmd=['apachectl', 'graceful'],
conftest_cmd=['apachectl', 'configtest'],
enmod=None,
dismod=None,
le_vhost_ext="-le-ssl.conf",
handle_modules=False,
handle_sites=False,
challenge_location="/etc/httpd/conf",
MOD_SSL_CONF_SRC=pkg_resources.resource_filename(
"certbot_apache", os.path.join("_internal", "options-ssl-apache.conf"))
)

View File

@@ -1,18 +1,18 @@
""" Distribution specific override class for CentOS family (RHEL, Fedora) """
import logging
from typing import cast
from typing import List
import pkg_resources
import zope.interface
from acme.magic_typing import List
from certbot import errors
from certbot import interfaces
from certbot import util
from certbot.compat import os
from certbot.errors import MisconfigurationError
from certbot_apache._internal import apache_util
from certbot_apache._internal import configurator
from certbot_apache._internal import parser
from certbot_apache._internal.configurator import OsOptions
logger = logging.getLogger(__name__)
@@ -21,7 +21,7 @@ logger = logging.getLogger(__name__)
class CentOSConfigurator(configurator.ApacheConfigurator):
"""CentOS specific ApacheConfigurator override class"""
OS_DEFAULTS = OsOptions(
OS_DEFAULTS = dict(
server_root="/etc/httpd",
vhost_root="/etc/httpd/conf.d",
vhost_files="*.conf",
@@ -31,7 +31,14 @@ class CentOSConfigurator(configurator.ApacheConfigurator):
restart_cmd=['apachectl', 'graceful'],
restart_cmd_alt=['apachectl', 'restart'],
conftest_cmd=['apachectl', 'configtest'],
enmod=None,
dismod=None,
le_vhost_ext="-le-ssl.conf",
handle_modules=False,
handle_sites=False,
challenge_location="/etc/httpd/conf.d",
MOD_SSL_CONF_SRC=pkg_resources.resource_filename(
"certbot_apache", os.path.join("_internal", "options-ssl-apache.conf"))
)
def config_test(self):
@@ -46,7 +53,7 @@ class CentOSConfigurator(configurator.ApacheConfigurator):
fedora = os_info[0].lower() == "fedora"
try:
super().config_test()
super(CentOSConfigurator, self).config_test()
except errors.MisconfigurationError:
if fedora:
self._try_restart_fedora()
@@ -64,22 +71,20 @@ class CentOSConfigurator(configurator.ApacheConfigurator):
raise errors.MisconfigurationError(str(err))
# Finish with actual config check to see if systemctl restart helped
super().config_test()
super(CentOSConfigurator, self).config_test()
def _prepare_options(self):
"""
Override the options dictionary initialization in order to support
alternative restart cmd used in CentOS.
"""
super()._prepare_options()
if not self.options.restart_cmd_alt: # pragma: no cover
raise ValueError("OS option restart_cmd_alt must be set for CentOS.")
self.options.restart_cmd_alt[0] = self.options.ctl
super(CentOSConfigurator, self)._prepare_options()
self.options["restart_cmd_alt"][0] = self.option("ctl")
def get_parser(self):
"""Initializes the ApacheParser"""
return CentOSParser(
self.options.server_root, self.options.vhost_root,
self.option("server_root"), self.option("vhost_root"),
self.version, configurator=self)
def _deploy_cert(self, *args, **kwargs): # pylint: disable=arguments-differ
@@ -88,7 +93,7 @@ class CentOSConfigurator(configurator.ApacheConfigurator):
has "LoadModule ssl_module..." before parsing the VirtualHost configuration
that was created by Certbot
"""
super()._deploy_cert(*args, **kwargs)
super(CentOSConfigurator, self)._deploy_cert(*args, **kwargs)
if self.version < (2, 4, 0):
self._deploy_loadmodule_ssl_if_needed()
@@ -100,9 +105,9 @@ class CentOSConfigurator(configurator.ApacheConfigurator):
loadmods = self.parser.find_dir("LoadModule", "ssl_module", exclude=False)
correct_ifmods: List[str] = []
loadmod_args: List[str] = []
loadmod_paths: List[str] = []
correct_ifmods = [] # type: List[str]
loadmod_args = [] # type: List[str]
loadmod_paths = [] # type: List[str]
for m in loadmods:
noarg_path = m.rpartition("/")[0]
path_args = self.parser.get_all_args(noarg_path)
@@ -116,9 +121,8 @@ class CentOSConfigurator(configurator.ApacheConfigurator):
else:
loadmod_args = path_args
centos_parser: CentOSParser = cast(CentOSParser, self.parser)
if centos_parser.not_modssl_ifmodule(noarg_path):
if centos_parser.loc["default"] in noarg_path:
if self.parser.not_modssl_ifmodule(noarg_path): # pylint: disable=no-member
if self.parser.loc["default"] in noarg_path:
# LoadModule already in the main configuration file
if ("ifmodule/" in noarg_path.lower() or
"ifmodule[1]" in noarg_path.lower()):
@@ -166,12 +170,12 @@ class CentOSParser(parser.ApacheParser):
def __init__(self, *args, **kwargs):
# CentOS specific configuration file for Apache
self.sysconfig_filep = "/etc/sysconfig/httpd"
super().__init__(*args, **kwargs)
super(CentOSParser, self).__init__(*args, **kwargs)
def update_runtime_variables(self):
""" Override for update_runtime_variables for custom parsing """
# Opportunistic, works if SELinux not enforced
super().update_runtime_variables()
super(CentOSParser, self).update_runtime_variables()
self.parse_sysconfig_var()
def parse_sysconfig_var(self):

View File

@@ -1,21 +1,31 @@
""" Distribution specific override class for macOS """
import pkg_resources
import zope.interface
from certbot import interfaces
from certbot.compat import os
from certbot_apache._internal import configurator
from certbot_apache._internal.configurator import OsOptions
@zope.interface.provider(interfaces.IPluginFactory)
class DarwinConfigurator(configurator.ApacheConfigurator):
"""macOS specific ApacheConfigurator override class"""
OS_DEFAULTS = OsOptions(
OS_DEFAULTS = dict(
server_root="/etc/apache2",
vhost_root="/etc/apache2/other",
vhost_files="*.conf",
logs_root="/var/log/apache2",
ctl="apachectl",
version_cmd=['apachectl', '-v'],
restart_cmd=['apachectl', 'graceful'],
conftest_cmd=['apachectl', 'configtest'],
enmod=None,
dismod=None,
le_vhost_ext="-le-ssl.conf",
handle_modules=False,
handle_sites=False,
challenge_location="/etc/apache2/other",
MOD_SSL_CONF_SRC=pkg_resources.resource_filename(
"certbot_apache", os.path.join("_internal", "options-ssl-apache.conf"))
)

View File

@@ -1,6 +1,7 @@
""" Distribution specific override class for Debian family (Ubuntu/Debian) """
import logging
import pkg_resources
import zope.interface
from certbot import errors
@@ -10,7 +11,6 @@ from certbot.compat import filesystem
from certbot.compat import os
from certbot_apache._internal import apache_util
from certbot_apache._internal import configurator
from certbot_apache._internal.configurator import OsOptions
logger = logging.getLogger(__name__)
@@ -19,11 +19,23 @@ logger = logging.getLogger(__name__)
class DebianConfigurator(configurator.ApacheConfigurator):
"""Debian specific ApacheConfigurator override class"""
OS_DEFAULTS = OsOptions(
OS_DEFAULTS = dict(
server_root="/etc/apache2",
vhost_root="/etc/apache2/sites-available",
vhost_files="*",
logs_root="/var/log/apache2",
ctl="apache2ctl",
version_cmd=['apache2ctl', '-v'],
restart_cmd=['apache2ctl', 'graceful'],
conftest_cmd=['apache2ctl', 'configtest'],
enmod="a2enmod",
dismod="a2dismod",
le_vhost_ext="-le-ssl.conf",
handle_modules=True,
handle_sites=True,
challenge_location="/etc/apache2",
MOD_SSL_CONF_SRC=pkg_resources.resource_filename(
"certbot_apache", os.path.join("_internal", "options-ssl-apache.conf"))
)
def enable_site(self, vhost):
@@ -48,7 +60,7 @@ class DebianConfigurator(configurator.ApacheConfigurator):
if not os.path.isdir(os.path.dirname(enabled_path)):
# For some reason, sites-enabled / sites-available do not exist
# Call the parent method
return super().enable_site(vhost)
return super(DebianConfigurator, self).enable_site(vhost)
self.reverter.register_file_creation(False, enabled_path)
try:
os.symlink(vhost.filep, enabled_path)
@@ -58,7 +70,7 @@ class DebianConfigurator(configurator.ApacheConfigurator):
# Already in shape
vhost.enabled = True
return None
logger.error(
logger.warning(
"Could not symlink %s to %s, got error: %s", enabled_path,
vhost.filep, err.strerror)
errstring = ("Encountered error while trying to enable a " +
@@ -122,11 +134,11 @@ class DebianConfigurator(configurator.ApacheConfigurator):
# Generate reversal command.
# Try to be safe here... check that we can probably reverse before
# applying enmod command
if not util.exe_exists(self.options.dismod):
if not util.exe_exists(self.option("dismod")):
raise errors.MisconfigurationError(
"Unable to find a2dismod, please make sure a2enmod and "
"a2dismod are configured correctly for certbot.")
self.reverter.register_undo_command(
temp, [self.options.dismod, "-f", mod_name])
util.run_script([self.options.enmod, mod_name])
temp, [self.option("dismod"), "-f", mod_name])
util.run_script([self.option("enmod"), mod_name])

View File

@@ -1,20 +1,21 @@
""" Distribution specific override class for Fedora 29+ """
import pkg_resources
import zope.interface
from certbot import errors
from certbot import interfaces
from certbot import util
from certbot.compat import os
from certbot_apache._internal import apache_util
from certbot_apache._internal import configurator
from certbot_apache._internal import parser
from certbot_apache._internal.configurator import OsOptions
@zope.interface.provider(interfaces.IPluginFactory)
class FedoraConfigurator(configurator.ApacheConfigurator):
"""Fedora 29+ specific ApacheConfigurator override class"""
OS_DEFAULTS = OsOptions(
OS_DEFAULTS = dict(
server_root="/etc/httpd",
vhost_root="/etc/httpd/conf.d",
vhost_files="*.conf",
@@ -24,7 +25,15 @@ class FedoraConfigurator(configurator.ApacheConfigurator):
restart_cmd=['apachectl', 'graceful'],
restart_cmd_alt=['apachectl', 'restart'],
conftest_cmd=['apachectl', 'configtest'],
enmod=None,
dismod=None,
le_vhost_ext="-le-ssl.conf",
handle_modules=False,
handle_sites=False,
challenge_location="/etc/httpd/conf.d",
MOD_SSL_CONF_SRC=pkg_resources.resource_filename(
# TODO: eventually newest version of Fedora will need their own config
"certbot_apache", os.path.join("_internal", "options-ssl-apache.conf"))
)
def config_test(self):
@@ -35,14 +44,14 @@ class FedoraConfigurator(configurator.ApacheConfigurator):
during the first (re)start of httpd.
"""
try:
super().config_test()
super(FedoraConfigurator, self).config_test()
except errors.MisconfigurationError:
self._try_restart_fedora()
def get_parser(self):
"""Initializes the ApacheParser"""
return FedoraParser(
self.options.server_root, self.options.vhost_root,
self.option("server_root"), self.option("vhost_root"),
self.version, configurator=self)
def _try_restart_fedora(self):
@@ -55,7 +64,7 @@ class FedoraConfigurator(configurator.ApacheConfigurator):
raise errors.MisconfigurationError(str(err))
# Finish with actual config check to see if systemctl restart helped
super().config_test()
super(FedoraConfigurator, self).config_test()
def _prepare_options(self):
"""
@@ -63,12 +72,10 @@ class FedoraConfigurator(configurator.ApacheConfigurator):
instead of httpd and so take advantages of this new bash script in newer versions
of Fedora to restart httpd.
"""
super()._prepare_options()
self.options.restart_cmd[0] = 'apachectl'
if not self.options.restart_cmd_alt: # pragma: no cover
raise ValueError("OS option restart_cmd_alt must be set for Fedora.")
self.options.restart_cmd_alt[0] = 'apachectl'
self.options.conftest_cmd[0] = 'apachectl'
super(FedoraConfigurator, self)._prepare_options()
self.options["restart_cmd"][0] = 'apachectl'
self.options["restart_cmd_alt"][0] = 'apachectl'
self.options["conftest_cmd"][0] = 'apachectl'
class FedoraParser(parser.ApacheParser):
@@ -76,12 +83,12 @@ class FedoraParser(parser.ApacheParser):
def __init__(self, *args, **kwargs):
# Fedora 29+ specific configuration file for Apache
self.sysconfig_filep = "/etc/sysconfig/httpd"
super().__init__(*args, **kwargs)
super(FedoraParser, self).__init__(*args, **kwargs)
def update_runtime_variables(self):
""" Override for update_runtime_variables for custom parsing """
# Opportunistic, works if SELinux not enforced
super().update_runtime_variables()
super(FedoraParser, self).update_runtime_variables()
self._parse_sysconfig_var()
def _parse_sysconfig_var(self):

View File

@@ -1,23 +1,36 @@
""" Distribution specific override class for Gentoo Linux """
import pkg_resources
import zope.interface
from certbot import interfaces
from certbot.compat import os
from certbot_apache._internal import apache_util
from certbot_apache._internal import configurator
from certbot_apache._internal import parser
from certbot_apache._internal.configurator import OsOptions
@zope.interface.provider(interfaces.IPluginFactory)
class GentooConfigurator(configurator.ApacheConfigurator):
"""Gentoo specific ApacheConfigurator override class"""
OS_DEFAULTS = OsOptions(
OS_DEFAULTS = dict(
server_root="/etc/apache2",
vhost_root="/etc/apache2/vhosts.d",
vhost_files="*.conf",
logs_root="/var/log/apache2",
ctl="apache2ctl",
version_cmd=['apache2ctl', '-v'],
restart_cmd=['apache2ctl', 'graceful'],
restart_cmd_alt=['apache2ctl', 'restart'],
conftest_cmd=['apache2ctl', 'configtest'],
enmod=None,
dismod=None,
le_vhost_ext="-le-ssl.conf",
handle_modules=False,
handle_sites=False,
challenge_location="/etc/apache2/vhosts.d",
MOD_SSL_CONF_SRC=pkg_resources.resource_filename(
"certbot_apache", os.path.join("_internal", "options-ssl-apache.conf"))
)
def _prepare_options(self):
@@ -25,15 +38,13 @@ class GentooConfigurator(configurator.ApacheConfigurator):
Override the options dictionary initialization in order to support
alternative restart cmd used in Gentoo.
"""
super()._prepare_options()
if not self.options.restart_cmd_alt: # pragma: no cover
raise ValueError("OS option restart_cmd_alt must be set for Gentoo.")
self.options.restart_cmd_alt[0] = self.options.ctl
super(GentooConfigurator, self)._prepare_options()
self.options["restart_cmd_alt"][0] = self.option("ctl")
def get_parser(self):
"""Initializes the ApacheParser"""
return GentooParser(
self.options.server_root, self.options.vhost_root,
self.option("server_root"), self.option("vhost_root"),
self.version, configurator=self)
@@ -42,7 +53,7 @@ class GentooParser(parser.ApacheParser):
def __init__(self, *args, **kwargs):
# Gentoo specific configuration file for Apache2
self.apacheconfig_filep = "/etc/conf.d/apache2"
super().__init__(*args, **kwargs)
super(GentooParser, self).__init__(*args, **kwargs)
def update_runtime_variables(self):
""" Override for update_runtime_variables for custom parsing """
@@ -58,7 +69,7 @@ class GentooParser(parser.ApacheParser):
def update_modules(self):
"""Get loaded modules from httpd process, and add them to DOM"""
mod_cmd = [self.configurator.options.ctl, "modules"]
mod_cmd = [self.configurator.option("ctl"), "modules"]
matches = apache_util.parse_from_subprocess(mod_cmd, r"(.*)_module")
for mod in matches:
self.add_mod(mod.strip())

View File

@@ -1,23 +1,31 @@
""" Distribution specific override class for OpenSUSE """
import pkg_resources
import zope.interface
from certbot import interfaces
from certbot.compat import os
from certbot_apache._internal import configurator
from certbot_apache._internal.configurator import OsOptions
@zope.interface.provider(interfaces.IPluginFactory)
class OpenSUSEConfigurator(configurator.ApacheConfigurator):
"""OpenSUSE specific ApacheConfigurator override class"""
OS_DEFAULTS = OsOptions(
OS_DEFAULTS = dict(
server_root="/etc/apache2",
vhost_root="/etc/apache2/vhosts.d",
vhost_files="*.conf",
ctl="apachectl",
version_cmd=['apachectl', '-v'],
restart_cmd=['apachectl', 'graceful'],
conftest_cmd=['apachectl', 'configtest'],
logs_root="/var/log/apache2",
ctl="apache2ctl",
version_cmd=['apache2ctl', '-v'],
restart_cmd=['apache2ctl', 'graceful'],
conftest_cmd=['apache2ctl', 'configtest'],
enmod="a2enmod",
dismod="a2dismod",
le_vhost_ext="-le-ssl.conf",
handle_modules=False,
handle_sites=False,
challenge_location="/etc/apache2/vhosts.d",
MOD_SSL_CONF_SRC=pkg_resources.resource_filename(
"certbot_apache", os.path.join("_internal", "options-ssl-apache.conf"))
)

View File

@@ -3,24 +3,22 @@ import copy
import fnmatch
import logging
import re
from typing import Dict
from typing import List
from typing import Optional
import sys
import six
from acme.magic_typing import Dict
from acme.magic_typing import List
from acme.magic_typing import Set
from certbot import errors
from certbot.compat import os
from certbot_apache._internal import apache_util
from certbot_apache._internal import constants
try:
from augeas import Augeas
except ImportError: # pragma: no cover
Augeas = None # type: ignore
logger = logging.getLogger(__name__)
class ApacheParser:
class ApacheParser(object):
"""Class handles the fine details of parsing the Apache Configuration.
.. todo:: Make parsing general... remove sites-available etc...
@@ -33,7 +31,7 @@ class ApacheParser:
"""
arg_var_interpreter = re.compile(r"\$\{[^ \}]*}")
fnmatch_chars = {"*", "?", "\\", "[", "]"}
fnmatch_chars = set(["*", "?", "\\", "[", "]"])
def __init__(self, root, vhostroot=None, version=(2, 4),
configurator=None):
@@ -45,7 +43,8 @@ class ApacheParser:
self.configurator = configurator
# Initialize augeas
self.aug = init_augeas()
self.aug = None
self.init_augeas()
if not self.check_aug_version():
raise errors.NotSupportedError(
@@ -53,9 +52,9 @@ class ApacheParser:
"version 1.2.0 or higher, please make sure you have you have "
"those installed.")
self.modules: Dict[str, Optional[str]] = {}
self.parser_paths: Dict[str, List[str]] = {}
self.variables: Dict[str, str] = {}
self.modules = set() # type: Set[str]
self.parser_paths = {} # type: Dict[str, List[str]]
self.variables = {} # type: Dict[str, str]
# Find configuration root and make sure augeas can parse it.
self.root = os.path.abspath(root)
@@ -81,13 +80,30 @@ class ApacheParser:
# Must also attempt to parse additional virtual host root
if vhostroot:
self.parse_file(os.path.abspath(vhostroot) + "/" +
self.configurator.options.vhost_files)
self.configurator.option("vhost_files"))
# check to see if there were unparsed define statements
if version < (2, 4):
if self.find_dir("Define", exclude=False):
raise errors.PluginError("Error parsing runtime variables")
def init_augeas(self):
""" Initialize the actual Augeas instance """
try:
import augeas
except ImportError: # pragma: no cover
raise errors.NoInstallationError("Problem in Augeas installation")
self.aug = augeas.Augeas(
# specify a directory to load our preferred lens from
loadpath=constants.AUGEAS_LENS_DIR,
# Do not save backup (we do it ourselves), do not load
# anything by default
flags=(augeas.Augeas.NONE |
augeas.Augeas.NO_MODL_AUTOLOAD |
augeas.Augeas.ENABLE_SPAN))
def check_parsing_errors(self, lens):
"""Verify Augeas can parse all of the lens files.
@@ -233,14 +249,14 @@ class ApacheParser:
def add_mod(self, mod_name):
"""Shortcut for updating parser modules."""
if mod_name + "_module" not in self.modules:
self.modules[mod_name + "_module"] = None
self.modules.add(mod_name + "_module")
if "mod_" + mod_name + ".c" not in self.modules:
self.modules["mod_" + mod_name + ".c"] = None
self.modules.add("mod_" + mod_name + ".c")
def reset_modules(self):
"""Reset the loaded modules list. This is called from cleanup to clear
temporarily loaded modules."""
self.modules = {}
self.modules = set()
self.update_modules()
self.parse_modules()
@@ -251,7 +267,7 @@ class ApacheParser:
the iteration issue. Else... parse and enable mods at same time.
"""
mods: Dict[str, str] = {}
mods = set() # type: Set[str]
matches = self.find_dir("LoadModule")
iterator = iter(matches)
# Make sure prev_size != cur_size for do: while: iteration
@@ -260,13 +276,13 @@ class ApacheParser:
while len(mods) != prev_size:
prev_size = len(mods)
for match_name, match_filename in zip(
for match_name, match_filename in six.moves.zip(
iterator, iterator):
mod_name = self.get_arg(match_name)
mod_filename = self.get_arg(match_filename)
if mod_name and mod_filename:
mods[mod_name] = mod_filename
mods[os.path.basename(mod_filename)[:-2] + "c"] = mod_filename
mods.add(mod_name)
mods.add(os.path.basename(mod_filename)[:-2] + "c")
else:
logger.debug("Could not read LoadModule directive from Augeas path: %s",
match_name[6:])
@@ -282,7 +298,7 @@ class ApacheParser:
def update_defines(self):
"""Updates the dictionary of known variables in the configuration"""
self.variables = apache_util.parse_defines(self.configurator.options.ctl)
self.variables = apache_util.parse_defines(self.configurator.option("ctl"))
def update_includes(self):
"""Get includes from httpd process, and add them to DOM if needed"""
@@ -292,7 +308,7 @@ class ApacheParser:
# configuration files
_ = self.find_dir("Include")
matches = apache_util.parse_includes(self.configurator.options.ctl)
matches = apache_util.parse_includes(self.configurator.option("ctl"))
if matches:
for i in matches:
if not self.parsed_in_current(i):
@@ -301,7 +317,7 @@ class ApacheParser:
def update_modules(self):
"""Get loaded modules from httpd process, and add them to DOM"""
matches = apache_util.parse_modules(self.configurator.options.ctl)
matches = apache_util.parse_modules(self.configurator.option("ctl"))
for mod in matches:
self.add_mod(mod.strip())
@@ -538,7 +554,7 @@ class ApacheParser:
else:
arg_suffix = "/*[self::arg=~regexp('%s')]" % case_i(arg)
ordered_matches: List[str] = []
ordered_matches = [] # type: List[str]
# TODO: Wildcards should be included in alphabetical order
# https://httpd.apache.org/docs/2.4/mod/core.html#include
@@ -605,7 +621,7 @@ class ApacheParser:
def exclude_dirs(self, matches):
"""Exclude directives that are not loaded into the configuration."""
filters = [("ifmodule", self.modules.keys()), ("ifdefine", self.variables)]
filters = [("ifmodule", self.modules), ("ifdefine", self.variables)]
valid_matches = []
@@ -646,25 +662,6 @@ class ApacheParser:
return True
def standard_path_from_server_root(self, arg):
"""Ensure paths are consistent and absolute
:param str arg: Argument of directive
:returns: Standardized argument path
:rtype: str
"""
# Remove beginning and ending quotes
arg = arg.strip("'\"")
# Standardize the include argument based on server root
if not arg.startswith("/"):
# Normpath will condense ../
arg = os.path.normpath(os.path.join(self.root, arg))
else:
arg = os.path.normpath(arg)
return arg
def _get_include_path(self, arg):
"""Converts an Apache Include directive into Augeas path.
@@ -685,7 +682,16 @@ class ApacheParser:
# if matchObj.group() != arg:
# logger.error("Error: Invalid regexp characters in %s", arg)
# return []
arg = self.standard_path_from_server_root(arg)
# Remove beginning and ending quotes
arg = arg.strip("'\"")
# Standardize the include argument based on server root
if not arg.startswith("/"):
# Normpath will condense ../
arg = os.path.normpath(os.path.join(self.root, arg))
else:
arg = os.path.normpath(arg)
# Attempts to add a transform to the file if one does not already exist
if os.path.isdir(arg):
@@ -716,6 +722,7 @@ class ApacheParser:
privileged users.
https://apr.apache.org/docs/apr/2.0/apr__fnmatch_8h_source.html
http://apache2.sourcearchive.com/documentation/2.2.16-6/apr__fnmatch_8h_source.html
:param str clean_fn_match: Apache style filename match, like globs
@@ -723,6 +730,9 @@ class ApacheParser:
:rtype: str
"""
if sys.version_info < (3, 6):
# This strips off final /Z(?ms)
return fnmatch.translate(clean_fn_match)[:-7]
# Since Python 3.6, it returns a different pattern like (?s:.*\.load)\Z
return fnmatch.translate(clean_fn_match)[4:-3] # pragma: no cover
@@ -780,7 +790,7 @@ class ApacheParser:
def _parsed_by_parser_paths(self, filep, paths):
"""Helper function that searches through provided paths and returns
True if file path is found in the set"""
for directory in paths:
for directory in paths.keys():
for filename in paths[directory]:
if fnmatch.fnmatch(filep, os.path.join(directory, filename)):
return True
@@ -926,8 +936,8 @@ def case_i(string):
:param str string: string to make case i regex
"""
return "".join("[" + c.upper() + c.lower() + "]"
if c.isalpha() else c for c in re.escape(string))
return "".join(["[" + c.upper() + c.lower() + "]"
if c.isalpha() else c for c in re.escape(string)])
def get_aug_path(file_path):
@@ -937,19 +947,3 @@ def get_aug_path(file_path):
"""
return "/files%s" % file_path
def init_augeas() -> Augeas:
""" Initialize the actual Augeas instance """
if not Augeas: # pragma: no cover
raise errors.NoInstallationError("Problem in Augeas installation")
return Augeas(
# specify a directory to load our preferred lens from
loadpath=constants.AUGEAS_LENS_DIR,
# Do not save backup (we do it ourselves), do not load
# anything by default
flags=(Augeas.NONE |
Augeas.NO_MODL_AUTOLOAD |
Augeas.ENABLE_SPAN))

View File

@@ -11,7 +11,7 @@ def validate_kwargs(kwargs, required_names):
:param list required_names: List of required parameter names.
"""
validated_kwargs = {}
validated_kwargs = dict()
for name in required_names:
try:
validated_kwargs[name] = kwargs.pop(name)
@@ -127,3 +127,45 @@ def directivenode_kwargs(kwargs):
parameters = kwargs.pop("parameters")
enabled = kwargs.pop("enabled")
return name, parameters, enabled, kwargs
def parameters_from_string(text):
"""Transforms a whitespace-separated string of parameters into a tuple of strings.
Ignores all whitespace outside quotations (matched single quotes or double quotes)
e.g. "parameter1 'parameter two'" => ("parameter1", "parameter two")
Mirrors parsing code in apache/httpd.
ap_getword_conf procedure for retrieving next token from a line:
https://github.com/apache/httpd/blob/5515e790adba6414c35ac19f8b6ffa0d7fc0051d/server/util.c#L787
substring_conf procedure for retrieving text between quotes:
https://github.com/apache/httpd/blob/5515e790adba6414c35ac19f8b6ffa0d7fc0051d/server/util.c#L759
:param str text: whitespace-separated string of apache arguments
:returns Tuple of strings extracted as parameters from text
"""
text = text.lstrip()
words = []
word = ""
escape = False
quote = None
for c in text:
if c.isspace() and not quote:
if word:
words.append(word)
word = ""
else:
word += c
if not quote and c in "\"\'":
quote = c
elif c == quote and not escape:
words.append(word[1:-1].replace("\\\\", "\\")
.replace("\\" + quote, quote))
word = ""
quote = None
escape = not escape and c == "\\"
if word:
words.append(word)
return tuple(words)

View File

@@ -1,19 +0,0 @@
# This file contains important security parameters. If you modify this file
# manually, Certbot will be unable to automatically provide future security
# updates. Instead, Certbot will print and log an error message with a path to
# the up-to-date file that you will need to refer to when manually updating
# this file.
SSLEngine on
# Intermediate configuration, tweak to your needs
SSLProtocol all -SSLv2 -SSLv3 -TLSv1 -TLSv1.1
SSLCipherSuite ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384
SSLHonorCipherOrder off
SSLSessionTickets off
SSLOptions +StrictRequire
# Add vhost name to log entries:
LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-agent}i\"" vhost_combined
LogFormat "%v %h %l %u %t \"%r\" %>s %b" vhost_common

View File

@@ -1,3 +1,3 @@
# Remember to update setup.py to match the package versions below.
acme[dev]==1.8.0
certbot[dev]==1.10.1
acme[dev]==0.29.0
certbot[dev]==1.1.0

View File

@@ -1,15 +1,19 @@
import sys
from setuptools import find_packages
from setuptools import setup
from setuptools.command.test import test as TestCommand
version = '1.16.0.dev0'
version = '1.3.0.dev0'
# Remember to update local-oldest-requirements.txt when changing the minimum
# acme/certbot version.
install_requires = [
'acme>=1.8.0',
'certbot>=1.10.1',
'acme>=0.29.0',
'certbot>=1.1.0',
'mock',
'python-augeas',
'setuptools>=39.0.1',
'setuptools',
'zope.component',
'zope.interface',
]
@@ -18,15 +22,30 @@ dev_extras = [
'apacheconfig>=0.3.2',
]
class PyTest(TestCommand):
user_options = []
def initialize_options(self):
TestCommand.initialize_options(self)
self.pytest_args = ''
def run_tests(self):
import shlex
# import here, cause outside the eggs aren't loaded
import pytest
errno = pytest.main(shlex.split(self.pytest_args))
sys.exit(errno)
setup(
name='certbot-apache',
version=version,
description="Apache plugin for Certbot",
url='https://github.com/letsencrypt/letsencrypt',
author="Certbot Project",
author_email='certbot-dev@eff.org',
author_email='client-dev@letsencrypt.org',
license='Apache License 2.0',
python_requires='>=3.6',
python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*',
classifiers=[
'Development Status :: 5 - Production/Stable',
'Environment :: Plugins',
@@ -34,11 +53,13 @@ setup(
'License :: OSI Approved :: Apache Software License',
'Operating System :: POSIX :: Linux',
'Programming Language :: Python',
'Programming Language :: Python :: 2',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.8',
'Programming Language :: Python :: 3.9',
'Topic :: Internet :: WWW/HTTP',
'Topic :: Security',
'Topic :: System :: Installation/Setup',
@@ -58,4 +79,7 @@ setup(
'apache = certbot_apache._internal.entrypoint:ENTRYPOINT',
],
},
test_suite='certbot_apache',
tests_require=["pytest"],
cmdclass={"test": PyTest},
)

View File

@@ -52,7 +52,7 @@ function Cleanup() {
# if our environment asks us to enable modules, do our best!
if [ "$1" = --debian-modules ] ; then
sudo apt-get install -y apache2
sudo apt-get install -y libapache2-mod-wsgi-py3
sudo apt-get install -y libapache2-mod-wsgi
sudo apt-get install -y libapache2-mod-macro
for mod in ssl rewrite macro wsgi deflate userdir version mime setenvif ; do

View File

@@ -1,41 +1,31 @@
"""Tests for AugeasParserNode classes"""
from typing import List
import mock
try:
import mock
except ImportError: # pragma: no cover
from unittest import mock # type: ignore
import os
import unittest
import util
try:
import apacheconfig # pylint: disable=import-error,unused-import
HAS_APACHECONFIG = True
except ImportError: # pragma: no cover
HAS_APACHECONFIG = False
from acme.magic_typing import List # pylint: disable=unused-import, no-name-in-module
from certbot import errors
from certbot_apache._internal import assertions
from certbot_apache._internal import augeasparser
def _get_augeasnode_mock(filepath):
""" Helper function for mocking out DualNode instance with an AugeasNode """
def augeasnode_mock(metadata):
return augeasparser.AugeasBlockNode(
name=assertions.PASS,
ancestor=None,
filepath=filepath,
metadata=metadata)
return augeasnode_mock
@unittest.skipIf(not HAS_APACHECONFIG, reason='Tests require apacheconfig dependency')
class AugeasParserNodeTest(util.ApacheTest): # pylint: disable=too-many-public-methods
"""Test AugeasParserNode using available test configurations"""
def setUp(self): # pylint: disable=arguments-differ
super().setUp()
super(AugeasParserNodeTest, self).setUp()
with mock.patch("certbot_apache._internal.configurator.ApacheConfigurator.get_parsernode_root") as mock_parsernode:
mock_parsernode.side_effect = _get_augeasnode_mock(
os.path.join(self.config_path, "apache2.conf"))
self.config = util.get_apache_configurator(
self.config_path, self.vhost_path, self.config_dir, self.work_dir, use_parsernode=True)
self.config = util.get_apache_configurator(
self.config_path, self.vhost_path, self.config_dir, self.work_dir, use_parsernode=True)
self.vh_truth = util.get_vh_truth(
self.temp_dir, "debian_apache_2_4/multiple_vhosts")
@@ -109,7 +99,7 @@ class AugeasParserNodeTest(util.ApacheTest): # pylint: disable=too-many-public-
def test_set_parameters(self):
servernames = self.config.parser_root.find_directives("servername")
names: List[str] = []
names = [] # type: List[str]
for servername in servernames:
names += servername.parameters
self.assertFalse("going_to_set_this" in names)
@@ -129,7 +119,7 @@ class AugeasParserNodeTest(util.ApacheTest): # pylint: disable=too-many-public-
name=servernames[0].name,
parameters=["test", "setting", "these"],
ancestor=assertions.PASS,
metadata=servernames[0].metadata
metadata=servernames[0].primary.metadata
)
self.assertTrue(mock_set.called)
self.assertEqual(
@@ -164,8 +154,8 @@ class AugeasParserNodeTest(util.ApacheTest): # pylint: disable=too-many-public-
self.assertTrue(found)
def test_add_child_comment(self):
newc = self.config.parser_root.add_child_comment("The content")
comments = self.config.parser_root.find_comments("The content")
newc = self.config.parser_root.primary.add_child_comment("The content")
comments = self.config.parser_root.primary.find_comments("The content")
self.assertEqual(len(comments), 1)
self.assertEqual(
newc.metadata["augeaspath"],
@@ -174,16 +164,16 @@ class AugeasParserNodeTest(util.ApacheTest): # pylint: disable=too-many-public-
self.assertEqual(newc.comment, comments[0].comment)
def test_delete_child(self):
listens = self.config.parser_root.find_directives("Listen")
listens = self.config.parser_root.primary.find_directives("Listen")
self.assertEqual(len(listens), 1)
self.config.parser_root.delete_child(listens[0])
self.config.parser_root.primary.delete_child(listens[0])
listens = self.config.parser_root.find_directives("Listen")
listens = self.config.parser_root.primary.find_directives("Listen")
self.assertEqual(len(listens), 0)
def test_delete_child_not_found(self):
listen = self.config.parser_root.find_directives("Listen")[0]
listen.metadata["augeaspath"] = "/files/something/nonexistent"
listen.primary.metadata["augeaspath"] = "/files/something/nonexistent"
self.assertRaises(
errors.PluginError,
@@ -196,10 +186,10 @@ class AugeasParserNodeTest(util.ApacheTest): # pylint: disable=too-many-public-
"NewBlock",
["first", "second"]
)
rpath, _, directive = nb.metadata["augeaspath"].rpartition("/")
rpath, _, directive = nb.primary.metadata["augeaspath"].rpartition("/")
self.assertEqual(
rpath,
self.config.parser_root.metadata["augeaspath"]
self.config.parser_root.primary.metadata["augeaspath"]
)
self.assertTrue(directive.startswith("NewBlock"))
@@ -208,8 +198,8 @@ class AugeasParserNodeTest(util.ApacheTest): # pylint: disable=too-many-public-
"Beginning",
position=0
)
parser = self.config.parser_root.parser
root_path = self.config.parser_root.metadata["augeaspath"]
parser = self.config.parser_root.primary.parser
root_path = self.config.parser_root.primary.metadata["augeaspath"]
# Get first child
first = parser.aug.match("{}/*[1]".format(root_path))
self.assertTrue(first[0].endswith("Beginning"))
@@ -218,8 +208,8 @@ class AugeasParserNodeTest(util.ApacheTest): # pylint: disable=too-many-public-
self.config.parser_root.add_child_block(
"VeryLast",
)
parser = self.config.parser_root.parser
root_path = self.config.parser_root.metadata["augeaspath"]
parser = self.config.parser_root.primary.parser
root_path = self.config.parser_root.primary.metadata["augeaspath"]
# Get last child
last = parser.aug.match("{}/*[last()]".format(root_path))
self.assertTrue(last[0].endswith("VeryLast"))
@@ -229,8 +219,8 @@ class AugeasParserNodeTest(util.ApacheTest): # pylint: disable=too-many-public-
"VeryLastAlt",
position=99999
)
parser = self.config.parser_root.parser
root_path = self.config.parser_root.metadata["augeaspath"]
parser = self.config.parser_root.primary.parser
root_path = self.config.parser_root.primary.metadata["augeaspath"]
# Get last child
last = parser.aug.match("{}/*[last()]".format(root_path))
self.assertTrue(last[0].endswith("VeryLastAlt"))
@@ -240,15 +230,15 @@ class AugeasParserNodeTest(util.ApacheTest): # pylint: disable=too-many-public-
"Middle",
position=5
)
parser = self.config.parser_root.parser
root_path = self.config.parser_root.metadata["augeaspath"]
parser = self.config.parser_root.primary.parser
root_path = self.config.parser_root.primary.metadata["augeaspath"]
# Augeas indices start at 1 :(
middle = parser.aug.match("{}/*[6]".format(root_path))
self.assertTrue(middle[0].endswith("Middle"))
def test_add_child_block_existing_name(self):
parser = self.config.parser_root.parser
root_path = self.config.parser_root.metadata["augeaspath"]
parser = self.config.parser_root.primary.parser
root_path = self.config.parser_root.primary.metadata["augeaspath"]
# There already exists a single VirtualHost in the base config
new_block = parser.aug.match("{}/VirtualHost[2]".format(root_path))
self.assertEqual(len(new_block), 0)
@@ -257,7 +247,7 @@ class AugeasParserNodeTest(util.ApacheTest): # pylint: disable=too-many-public-
)
new_block = parser.aug.match("{}/VirtualHost[2]".format(root_path))
self.assertEqual(len(new_block), 1)
self.assertTrue(vh.metadata["augeaspath"].endswith("VirtualHost[2]"))
self.assertTrue(vh.primary.metadata["augeaspath"].endswith("VirtualHost[2]"))
def test_node_init_error_bad_augeaspath(self):
from certbot_apache._internal.augeasparser import AugeasBlockNode
@@ -298,7 +288,7 @@ class AugeasParserNodeTest(util.ApacheTest): # pylint: disable=too-many-public-
["with", "parameters"],
position=0
)
dirs = self.config.parser_root.find_directives("ThisWasAdded")
dirs = self.config.parser_root.primary.find_directives("ThisWasAdded")
self.assertEqual(len(dirs), 1)
self.assertEqual(dirs[0].parameters, ("with", "parameters"))
# The new directive was added to the very first line of the config
@@ -332,6 +322,6 @@ class AugeasParserNodeTest(util.ApacheTest): # pylint: disable=too-many-public-
self.assertTrue(nonmacro_test)
def test_find_ancestors_bad_path(self):
self.config.parser_root.metadata["augeaspath"] = ""
ancs = self.config.parser_root.find_ancestors("Anything")
self.config.parser_root.primary.metadata["augeaspath"] = ""
ancs = self.config.parser_root.primary.find_ancestors("Anything")
self.assertEqual(len(ancs), 0)

View File

@@ -3,10 +3,8 @@
import re
import unittest
try:
import mock
except ImportError: # pragma: no cover
from unittest import mock # type: ignore
import mock
import six # pylint: disable=unused-import # six is used in mock.patch()
from certbot import errors
from certbot_apache._internal import constants
@@ -18,14 +16,14 @@ class AutoHSTSTest(util.ApacheTest):
# pylint: disable=protected-access
def setUp(self): # pylint: disable=arguments-differ
super().setUp()
super(AutoHSTSTest, self).setUp()
self.config = util.get_apache_configurator(
self.config_path, self.vhost_path, self.config_dir, self.work_dir)
self.config.parser.modules["headers_module"] = None
self.config.parser.modules["mod_headers.c"] = None
self.config.parser.modules["ssl_module"] = None
self.config.parser.modules["mod_ssl.c"] = None
self.config.parser.modules.add("headers_module")
self.config.parser.modules.add("mod_headers.c")
self.config.parser.modules.add("ssl_module")
self.config.parser.modules.add("mod_ssl.c")
self.vh_truth = util.get_vh_truth(
self.temp_dir, "debian_apache_2_4/multiple_vhosts")
@@ -44,8 +42,8 @@ class AutoHSTSTest(util.ApacheTest):
@mock.patch("certbot_apache._internal.configurator.ApacheConfigurator.restart")
@mock.patch("certbot_apache._internal.configurator.ApacheConfigurator.enable_mod")
def test_autohsts_enable_headers_mod(self, mock_enable, _restart):
self.config.parser.modules.pop("headers_module", None)
self.config.parser.modules.pop("mod_header.c", None)
self.config.parser.modules.discard("headers_module")
self.config.parser.modules.discard("mod_header.c")
self.config.enable_autohsts(mock.MagicMock(), ["ocspvhost.com"])
self.assertTrue(mock_enable.called)
@@ -146,7 +144,7 @@ class AutoHSTSTest(util.ApacheTest):
@mock.patch("certbot_apache._internal.display_ops.select_vhost")
def test_autohsts_no_ssl_vhost(self, mock_select):
mock_select.return_value = self.vh_truth[0]
with mock.patch("certbot_apache._internal.configurator.logger.error") as mock_log:
with mock.patch("certbot_apache._internal.configurator.logger.warning") as mock_log:
self.assertRaises(errors.PluginError,
self.config.enable_autohsts,
mock.MagicMock(), "invalid.example.com")
@@ -179,7 +177,7 @@ class AutoHSTSTest(util.ApacheTest):
self.config._autohsts_fetch_state()
self.config._autohsts["orphan_id"] = {"laststep": 999, "timestamp": 0}
self.config._autohsts_save_state()
with mock.patch("certbot_apache._internal.configurator.logger.error") as mock_log:
with mock.patch("certbot_apache._internal.configurator.logger.warning") as mock_log:
self.config.deploy_autohsts(mock.MagicMock())
self.assertTrue(mock_log.called)
self.assertTrue(

View File

@@ -1,6 +1,5 @@
"""Test for certbot_apache._internal.configurator for CentOS 6 overrides"""
import unittest
from unittest import mock
from certbot.compat import os
from certbot.errors import MisconfigurationError
@@ -20,12 +19,12 @@ def get_vh_truth(temp_dir, config_name):
obj.VirtualHost(
os.path.join(prefix, "test.example.com.conf"),
os.path.join(aug_pre, "test.example.com.conf/VirtualHost"),
{obj.Addr.fromstring("*:80")},
set([obj.Addr.fromstring("*:80")]),
False, True, "test.example.com"),
obj.VirtualHost(
os.path.join(prefix, "ssl.conf"),
os.path.join(aug_pre, "ssl.conf/VirtualHost"),
{obj.Addr.fromstring("_default_:443")},
set([obj.Addr.fromstring("_default_:443")]),
True, True, None)
]
return vh_truth
@@ -37,9 +36,9 @@ class CentOS6Tests(util.ApacheTest):
test_dir = "centos6_apache/apache"
config_root = "centos6_apache/apache/httpd"
vhost_root = "centos6_apache/apache/httpd/conf.d"
super().setUp(test_dir=test_dir,
config_root=config_root,
vhost_root=vhost_root)
super(CentOS6Tests, self).setUp(test_dir=test_dir,
config_root=config_root,
vhost_root=vhost_root)
self.config = util.get_apache_configurator(
self.config_path, self.vhost_path, self.config_dir, self.work_dir,
@@ -66,8 +65,7 @@ class CentOS6Tests(util.ApacheTest):
raise Exception("Missed: %s" % vhost) # pragma: no cover
self.assertEqual(found, 2)
@mock.patch("certbot_apache._internal.configurator.display_util.notify")
def test_loadmod_default(self, unused_mock_notify):
def test_loadmod_default(self):
ssl_loadmods = self.config.parser.find_dir(
"LoadModule", "ssl_module", exclude=False)
self.assertEqual(len(ssl_loadmods), 1)
@@ -97,8 +95,7 @@ class CentOS6Tests(util.ApacheTest):
ifmod_args = self.config.parser.get_all_args(lm[:-17])
self.assertTrue("!mod_ssl.c" in ifmod_args)
@mock.patch("certbot_apache._internal.configurator.display_util.notify")
def test_loadmod_multiple(self, unused_mock_notify):
def test_loadmod_multiple(self):
sslmod_args = ["ssl_module", "modules/mod_ssl.so"]
# Adds another LoadModule to main httpd.conf in addtition to ssl.conf
self.config.parser.add_dir(self.config.parser.loc["default"], "LoadModule",
@@ -107,7 +104,7 @@ class CentOS6Tests(util.ApacheTest):
pre_loadmods = self.config.parser.find_dir(
"LoadModule", "ssl_module", exclude=False)
# LoadModules are not within IfModule blocks
self.assertFalse(any("ifmodule" in m.lower() for m in pre_loadmods))
self.assertFalse(any(["ifmodule" in m.lower() for m in pre_loadmods]))
self.config.assoc["test.example.com"] = self.vh_truth[0]
self.config.deploy_cert(
"random.demo", "example/cert.pem", "example/key.pem",
@@ -118,8 +115,7 @@ class CentOS6Tests(util.ApacheTest):
for mod in post_loadmods:
self.assertTrue(self.config.parser.not_modssl_ifmodule(mod)) #pylint: disable=no-member
@mock.patch("certbot_apache._internal.configurator.display_util.notify")
def test_loadmod_rootconf_exists(self, unused_mock_notify):
def test_loadmod_rootconf_exists(self):
sslmod_args = ["ssl_module", "modules/mod_ssl.so"]
rootconf_ifmod = self.config.parser.get_ifmod(
parser.get_aug_path(self.config.parser.loc["default"]),
@@ -146,8 +142,7 @@ class CentOS6Tests(util.ApacheTest):
self.config.parser.get_all_args(mods[0][:-7]),
sslmod_args)
@mock.patch("certbot_apache._internal.configurator.display_util.notify")
def test_neg_loadmod_already_on_path(self, unused_mock_notify):
def test_neg_loadmod_already_on_path(self):
loadmod_args = ["ssl_module", "modules/mod_ssl.so"]
ifmod = self.config.parser.get_ifmod(
self.vh_truth[1].path, "!mod_ssl.c", beginning=True)
@@ -190,8 +185,7 @@ class CentOS6Tests(util.ApacheTest):
# Make sure that none was changed
self.assertEqual(pre_matches, post_matches)
@mock.patch("certbot_apache._internal.configurator.display_util.notify")
def test_loadmod_not_found(self, unused_mock_notify):
def test_loadmod_not_found(self):
# Remove all existing LoadModule ssl_module... directives
orig_loadmods = self.config.parser.find_dir("LoadModule",
"ssl_module",

View File

@@ -1,10 +1,7 @@
"""Test for certbot_apache._internal.configurator for Centos overrides"""
import unittest
try:
import mock
except ImportError: # pragma: no cover
from unittest import mock # type: ignore
import mock
from certbot import errors
from certbot.compat import filesystem
@@ -24,12 +21,12 @@ def get_vh_truth(temp_dir, config_name):
obj.VirtualHost(
os.path.join(prefix, "centos.example.com.conf"),
os.path.join(aug_pre, "centos.example.com.conf/VirtualHost"),
{obj.Addr.fromstring("*:80")},
set([obj.Addr.fromstring("*:80")]),
False, True, "centos.example.com"),
obj.VirtualHost(
os.path.join(prefix, "ssl.conf"),
os.path.join(aug_pre, "ssl.conf/VirtualHost"),
{obj.Addr.fromstring("_default_:443")},
set([obj.Addr.fromstring("_default_:443")]),
True, True, None)
]
return vh_truth
@@ -41,9 +38,9 @@ class FedoraRestartTest(util.ApacheTest):
test_dir = "centos7_apache/apache"
config_root = "centos7_apache/apache/httpd"
vhost_root = "centos7_apache/apache/httpd/conf.d"
super().setUp(test_dir=test_dir,
config_root=config_root,
vhost_root=vhost_root)
super(FedoraRestartTest, self).setUp(test_dir=test_dir,
config_root=config_root,
vhost_root=vhost_root)
self.config = util.get_apache_configurator(
self.config_path, self.vhost_path, self.config_dir, self.work_dir,
os_info="fedora_old")
@@ -96,9 +93,9 @@ class MultipleVhostsTestCentOS(util.ApacheTest):
test_dir = "centos7_apache/apache"
config_root = "centos7_apache/apache/httpd"
vhost_root = "centos7_apache/apache/httpd/conf.d"
super().setUp(test_dir=test_dir,
config_root=config_root,
vhost_root=vhost_root)
super(MultipleVhostsTestCentOS, self).setUp(test_dir=test_dir,
config_root=config_root,
vhost_root=vhost_root)
self.config = util.get_apache_configurator(
self.config_path, self.vhost_path, self.config_dir, self.work_dir,
@@ -129,7 +126,7 @@ class MultipleVhostsTestCentOS(util.ApacheTest):
return mod_val
return ""
mock_get.side_effect = mock_get_cfg
self.config.parser.modules = {}
self.config.parser.modules = set()
self.config.parser.variables = {}
with mock.patch("certbot.util.get_os_info") as mock_osi:
@@ -140,7 +137,7 @@ class MultipleVhostsTestCentOS(util.ApacheTest):
self.assertEqual(mock_get.call_count, 3)
self.assertEqual(len(self.config.parser.modules), 4)
self.assertEqual(len(self.config.parser.variables), 2)
self.assertTrue("TEST2" in self.config.parser.variables)
self.assertTrue("TEST2" in self.config.parser.variables.keys())
self.assertTrue("mod_another.c" in self.config.parser.modules)
def test_get_virtual_hosts(self):
@@ -172,11 +169,11 @@ class MultipleVhostsTestCentOS(util.ApacheTest):
mock_osi.return_value = ("centos", "7")
self.config.parser.update_runtime_variables()
self.assertTrue("mock_define" in self.config.parser.variables)
self.assertTrue("mock_define_too" in self.config.parser.variables)
self.assertTrue("mock_value" in self.config.parser.variables)
self.assertTrue("mock_define" in self.config.parser.variables.keys())
self.assertTrue("mock_define_too" in self.config.parser.variables.keys())
self.assertTrue("mock_value" in self.config.parser.variables.keys())
self.assertEqual("TRUE", self.config.parser.variables["mock_value"])
self.assertTrue("MOCK_NOSEP" in self.config.parser.variables)
self.assertTrue("MOCK_NOSEP" in self.config.parser.variables.keys())
self.assertEqual("NOSEP_VAL", self.config.parser.variables["NOSEP_TWO"])
@mock.patch("certbot_apache._internal.configurator.util.run_script")

View File

@@ -11,7 +11,7 @@ class ComplexParserTest(util.ParserTest):
"""Apache Parser Test."""
def setUp(self): # pylint: disable=arguments-differ
super().setUp(
super(ComplexParserTest, self).setUp(
"complex_parsing", "complex_parsing")
self.setup_variables()

View File

@@ -2,10 +2,7 @@
import shutil
import unittest
try:
import mock
except ImportError: # pragma: no cover
from unittest import mock # type: ignore
import mock
from certbot import errors
import util
@@ -16,7 +13,7 @@ class ConfiguratorReverterTest(util.ApacheTest):
def setUp(self): # pylint: disable=arguments-differ
super().setUp()
super(ConfiguratorReverterTest, self).setUp()
self.config = util.get_apache_configurator(
self.config_path, self.vhost_path, self.config_dir, self.work_dir)

View File

@@ -6,10 +6,8 @@ import socket
import tempfile
import unittest
try:
import mock
except ImportError: # pragma: no cover
from unittest import mock # type: ignore
import mock
import six # pylint: disable=unused-import # six is used in mock.patch()
from acme import challenges
from certbot import achallenges
@@ -30,7 +28,7 @@ class MultipleVhostsTest(util.ApacheTest):
"""Test two standard well-configured HTTP vhosts."""
def setUp(self): # pylint: disable=arguments-differ
super().setUp()
super(MultipleVhostsTest, self).setUp()
self.config = util.get_apache_configurator(
self.config_path, self.vhost_path, self.config_dir, self.work_dir)
@@ -101,11 +99,11 @@ class MultipleVhostsTest(util.ApacheTest):
parserargs = ["server_root", "enmod", "dismod", "le_vhost_ext",
"vhost_root", "logs_root", "challenge_location",
"handle_modules", "handle_sites", "ctl"]
exp = {}
exp = dict()
for k in ApacheConfigurator.OS_DEFAULTS.__dict__.keys():
for k in ApacheConfigurator.OS_DEFAULTS:
if k in parserargs:
exp[k.replace("_", "-")] = getattr(ApacheConfigurator.OS_DEFAULTS, k)
exp[k.replace("_", "-")] = ApacheConfigurator.OS_DEFAULTS[k]
# Special cases
exp["vhost-root"] = None
@@ -128,22 +126,25 @@ class MultipleVhostsTest(util.ApacheTest):
def test_all_configurators_defaults_defined(self):
from certbot_apache._internal.entrypoint import OVERRIDE_CLASSES
from certbot_apache._internal.configurator import ApacheConfigurator
parameters = set(ApacheConfigurator.OS_DEFAULTS.__dict__.keys())
parameters = set(ApacheConfigurator.OS_DEFAULTS.keys())
for cls in OVERRIDE_CLASSES.values():
self.assertTrue(parameters.issubset(set(cls.OS_DEFAULTS.__dict__.keys())))
self.assertTrue(parameters.issubset(set(cls.OS_DEFAULTS.keys())))
def test_constant(self):
self.assertTrue("debian_apache_2_4/multiple_vhosts/apache" in
self.config.options.server_root)
self.config.option("server_root"))
self.assertEqual(self.config.option("nonexistent"), None)
@certbot_util.patch_get_utility()
def test_get_all_names(self, mock_getutility):
mock_utility = mock_getutility()
mock_utility.notification = mock.MagicMock(return_value=True)
names = self.config.get_all_names()
self.assertEqual(names, {"certbot.demo", "ocspvhost.com", "encryption-example.demo",
self.assertEqual(names, set(
["certbot.demo", "ocspvhost.com", "encryption-example.demo",
"nonsym.link", "vhost.in.rootconf", "www.certbot.demo",
"duplicate.example.com"})
"duplicate.example.com"]
))
@certbot_util.patch_get_utility()
@mock.patch("certbot_apache._internal.configurator.socket.gethostbyaddr")
@@ -153,9 +154,9 @@ class MultipleVhostsTest(util.ApacheTest):
mock_utility.notification.return_value = True
vhost = obj.VirtualHost(
"fp", "ap",
{obj.Addr(("8.8.8.8", "443")),
set([obj.Addr(("8.8.8.8", "443")),
obj.Addr(("zombo.com",)),
obj.Addr(("192.168.1.2"))},
obj.Addr(("192.168.1.2"))]),
True, False)
self.config.vhosts.append(vhost)
@@ -184,7 +185,7 @@ class MultipleVhostsTest(util.ApacheTest):
def test_bad_servername_alias(self):
ssl_vh1 = obj.VirtualHost(
"fp1", "ap1", {obj.Addr(("*", "443"))},
"fp1", "ap1", set([obj.Addr(("*", "443"))]),
True, False)
# pylint: disable=protected-access
self.config._add_servernames(ssl_vh1)
@@ -197,7 +198,7 @@ class MultipleVhostsTest(util.ApacheTest):
# pylint: disable=protected-access
self.config._add_servernames(self.vh_truth[2])
self.assertEqual(
self.vh_truth[2].get_names(), {"*.le.co", "ip-172-30-0-17"})
self.vh_truth[2].get_names(), set(["*.le.co", "ip-172-30-0-17"]))
def test_get_virtual_hosts(self):
"""Make sure all vhosts are being properly found."""
@@ -268,7 +269,7 @@ class MultipleVhostsTest(util.ApacheTest):
def test_choose_vhost_select_vhost_conflicting_non_ssl(self, mock_select):
mock_select.return_value = self.vh_truth[3]
conflicting_vhost = obj.VirtualHost(
"path", "aug_path", {obj.Addr.fromstring("*:443")},
"path", "aug_path", set([obj.Addr.fromstring("*:443")]),
True, True)
self.config.vhosts.append(conflicting_vhost)
@@ -277,14 +278,14 @@ class MultipleVhostsTest(util.ApacheTest):
def test_find_best_http_vhost_default(self):
vh = obj.VirtualHost(
"fp", "ap", {obj.Addr.fromstring("_default_:80")}, False, True)
"fp", "ap", set([obj.Addr.fromstring("_default_:80")]), False, True)
self.config.vhosts = [vh]
self.assertEqual(self.config.find_best_http_vhost("foo.bar", False), vh)
def test_find_best_http_vhost_port(self):
port = "8080"
vh = obj.VirtualHost(
"fp", "ap", {obj.Addr.fromstring("*:" + port)},
"fp", "ap", set([obj.Addr.fromstring("*:" + port)]),
False, True, "encryption-example.demo")
self.config.vhosts.append(vh)
self.assertEqual(self.config.find_best_http_vhost("foo.bar", False, port), vh)
@@ -312,8 +313,8 @@ class MultipleVhostsTest(util.ApacheTest):
def test_find_best_vhost_variety(self):
# pylint: disable=protected-access
ssl_vh = obj.VirtualHost(
"fp", "ap", {obj.Addr(("*", "443")),
obj.Addr(("zombo.com",))},
"fp", "ap", set([obj.Addr(("*", "443")),
obj.Addr(("zombo.com",))]),
True, False)
self.config.vhosts.append(ssl_vh)
self.assertEqual(self.config._find_best_vhost("zombo.com"), ssl_vh)
@@ -337,13 +338,12 @@ class MultipleVhostsTest(util.ApacheTest):
vhosts = self.config._non_default_vhosts(self.config.vhosts)
self.assertEqual(len(vhosts), 10)
@mock.patch('certbot_apache._internal.configurator.display_util.notify')
def test_deploy_cert_enable_new_vhost(self, unused_mock_notify):
def test_deploy_cert_enable_new_vhost(self):
# Create
ssl_vhost = self.config.make_vhost_ssl(self.vh_truth[0])
self.config.parser.modules["ssl_module"] = None
self.config.parser.modules["mod_ssl.c"] = None
self.config.parser.modules["socache_shmcb_module"] = None
self.config.parser.modules.add("ssl_module")
self.config.parser.modules.add("mod_ssl.c")
self.config.parser.modules.add("socache_shmcb_module")
self.assertFalse(ssl_vhost.enabled)
self.config.deploy_cert(
@@ -376,11 +376,10 @@ class MultipleVhostsTest(util.ApacheTest):
self.fail("Include shouldn't be added, as patched find_dir 'finds' existing one") \
# pragma: no cover
@mock.patch('certbot_apache._internal.configurator.display_util.notify')
def test_deploy_cert(self, unused_mock_notify):
self.config.parser.modules["ssl_module"] = None
self.config.parser.modules["mod_ssl.c"] = None
self.config.parser.modules["socache_shmcb_module"] = None
def test_deploy_cert(self):
self.config.parser.modules.add("ssl_module")
self.config.parser.modules.add("mod_ssl.c")
self.config.parser.modules.add("socache_shmcb_module")
# Patch _add_dummy_ssl_directives to make sure we write them correctly
# pylint: disable=protected-access
orig_add_dummy = self.config._add_dummy_ssl_directives
@@ -455,6 +454,41 @@ class MultipleVhostsTest(util.ApacheTest):
"SSLCertificateChainFile", "two/cert_chain.pem",
self.vh_truth[1].path))
def test_deploy_cert_invalid_vhost(self):
"""For test cases where the `ApacheConfigurator` class' `_deploy_cert`
method is called with an invalid vhost parameter. Currently this tests
that a PluginError is appropriately raised when important directives
are missing in an SSL module."""
self.config.parser.modules.add("ssl_module")
self.config.parser.modules.add("mod_ssl.c")
self.config.parser.modules.add("socache_shmcb_module")
def side_effect(*args):
"""Mocks case where an SSLCertificateFile directive can be found
but an SSLCertificateKeyFile directive is missing."""
if "SSLCertificateFile" in args:
return ["example/cert.pem"]
return []
mock_find_dir = mock.MagicMock(return_value=[])
mock_find_dir.side_effect = side_effect
self.config.parser.find_dir = mock_find_dir
# Get the default 443 vhost
self.config.assoc["random.demo"] = self.vh_truth[1]
self.assertRaises(
errors.PluginError, self.config.deploy_cert, "random.demo",
"example/cert.pem", "example/key.pem", "example/cert_chain.pem")
# Remove side_effect to mock case where both SSLCertificateFile
# and SSLCertificateKeyFile directives are missing
self.config.parser.find_dir.side_effect = None
self.assertRaises(
errors.PluginError, self.config.deploy_cert, "random.demo",
"example/cert.pem", "example/key.pem", "example/cert_chain.pem")
def test_is_name_vhost(self):
addr = obj.Addr.fromstring("*:80")
self.assertTrue(self.config.is_name_vhost(addr))
@@ -510,8 +544,7 @@ class MultipleVhostsTest(util.ApacheTest):
call_found = True
self.assertTrue(call_found)
@mock.patch("certbot_apache._internal.parser.ApacheParser.reset_modules")
def test_prepare_server_https(self, mock_reset):
def test_prepare_server_https(self):
mock_enable = mock.Mock()
self.config.enable_mod = mock_enable
@@ -537,8 +570,7 @@ class MultipleVhostsTest(util.ApacheTest):
self.assertEqual(mock_add_dir.call_count, 2)
@mock.patch("certbot_apache._internal.parser.ApacheParser.reset_modules")
def test_prepare_server_https_named_listen(self, mock_reset):
def test_prepare_server_https_named_listen(self):
mock_find = mock.Mock()
mock_find.return_value = ["test1", "test2", "test3"]
mock_get = mock.Mock()
@@ -576,8 +608,7 @@ class MultipleVhostsTest(util.ApacheTest):
# self.config.prepare_server_https("8080", temp=True)
# self.assertEqual(self.listens, 0)
@mock.patch("certbot_apache._internal.parser.ApacheParser.reset_modules")
def test_prepare_server_https_needed_listen(self, mock_reset):
def test_prepare_server_https_needed_listen(self):
mock_find = mock.Mock()
mock_find.return_value = ["test1", "test2"]
mock_get = mock.Mock()
@@ -593,8 +624,8 @@ class MultipleVhostsTest(util.ApacheTest):
self.config.prepare_server_https("443")
self.assertEqual(mock_add_dir.call_count, 1)
@mock.patch("certbot_apache._internal.parser.ApacheParser.reset_modules")
def test_prepare_server_https_mixed_listen(self, mock_reset):
def test_prepare_server_https_mixed_listen(self):
mock_find = mock.Mock()
mock_find.return_value = ["test1", "test2"]
mock_get = mock.Mock()
@@ -651,7 +682,7 @@ class MultipleVhostsTest(util.ApacheTest):
self.assertEqual(ssl_vhost.path,
"/files" + ssl_vhost.filep + "/IfModule/Virtualhost")
self.assertEqual(len(ssl_vhost.addrs), 1)
self.assertEqual({obj.Addr.fromstring("*:443")}, ssl_vhost.addrs)
self.assertEqual(set([obj.Addr.fromstring("*:443")]), ssl_vhost.addrs)
self.assertEqual(ssl_vhost.name, "encryption-example.demo")
self.assertTrue(ssl_vhost.ssl)
self.assertFalse(ssl_vhost.enabled)
@@ -726,7 +757,7 @@ class MultipleVhostsTest(util.ApacheTest):
# This calls open
self.config.reverter.register_file_creation = mock.Mock()
mock_open.side_effect = IOError
with mock.patch("builtins.open", mock_open):
with mock.patch("six.moves.builtins.open", mock_open):
self.assertRaises(
errors.PluginError,
self.config.make_vhost_ssl, self.vh_truth[0])
@@ -873,10 +904,10 @@ class MultipleVhostsTest(util.ApacheTest):
@mock.patch("certbot_apache._internal.display_ops.select_vhost")
@mock.patch("certbot.util.exe_exists")
def test_enhance_unknown_vhost(self, mock_exe, mock_sel_vhost, mock_get):
self.config.parser.modules["rewrite_module"] = None
self.config.parser.modules.add("rewrite_module")
mock_exe.return_value = True
ssl_vh1 = obj.VirtualHost(
"fp1", "ap1", {obj.Addr(("*", "443"))},
"fp1", "ap1", set([obj.Addr(("*", "443"))]),
True, False)
ssl_vh1.name = "satoshi.com"
self.config.vhosts.append(ssl_vh1)
@@ -893,7 +924,7 @@ class MultipleVhostsTest(util.ApacheTest):
self.config.enhance, "certbot.demo", "unknown_enhancement")
def test_enhance_no_ssl_vhost(self):
with mock.patch("certbot_apache._internal.configurator.logger.error") as mock_log:
with mock.patch("certbot_apache._internal.configurator.logger.warning") as mock_log:
self.assertRaises(errors.PluginError, self.config.enhance,
"certbot.demo", "redirect")
# Check that correct logger.warning was printed
@@ -911,8 +942,8 @@ class MultipleVhostsTest(util.ApacheTest):
@mock.patch("certbot.util.exe_exists")
def test_ocsp_stapling(self, mock_exe):
self.config.parser.update_runtime_variables = mock.Mock()
self.config.parser.modules["mod_ssl.c"] = None
self.config.parser.modules["socache_shmcb_module"] = None
self.config.parser.modules.add("mod_ssl.c")
self.config.parser.modules.add("socache_shmcb_module")
self.config.get_version = mock.Mock(return_value=(2, 4, 7))
mock_exe.return_value = True
@@ -938,8 +969,8 @@ class MultipleVhostsTest(util.ApacheTest):
@mock.patch("certbot.util.exe_exists")
def test_ocsp_stapling_twice(self, mock_exe):
self.config.parser.update_runtime_variables = mock.Mock()
self.config.parser.modules["mod_ssl.c"] = None
self.config.parser.modules["socache_shmcb_module"] = None
self.config.parser.modules.add("mod_ssl.c")
self.config.parser.modules.add("socache_shmcb_module")
self.config.get_version = mock.Mock(return_value=(2, 4, 7))
mock_exe.return_value = True
@@ -966,8 +997,8 @@ class MultipleVhostsTest(util.ApacheTest):
def test_ocsp_unsupported_apache_version(self, mock_exe):
mock_exe.return_value = True
self.config.parser.update_runtime_variables = mock.Mock()
self.config.parser.modules["mod_ssl.c"] = None
self.config.parser.modules["socache_shmcb_module"] = None
self.config.parser.modules.add("mod_ssl.c")
self.config.parser.modules.add("socache_shmcb_module")
self.config.get_version = mock.Mock(return_value=(2, 2, 0))
self.config.choose_vhost("certbot.demo")
@@ -977,7 +1008,7 @@ class MultipleVhostsTest(util.ApacheTest):
def test_get_http_vhost_third_filter(self):
ssl_vh = obj.VirtualHost(
"fp", "ap", {obj.Addr(("*", "443"))},
"fp", "ap", set([obj.Addr(("*", "443"))]),
True, False)
ssl_vh.name = "satoshi.com"
self.config.vhosts.append(ssl_vh)
@@ -990,8 +1021,8 @@ class MultipleVhostsTest(util.ApacheTest):
@mock.patch("certbot.util.exe_exists")
def test_http_header_hsts(self, mock_exe, _):
self.config.parser.update_runtime_variables = mock.Mock()
self.config.parser.modules["mod_ssl.c"] = None
self.config.parser.modules["headers_module"] = None
self.config.parser.modules.add("mod_ssl.c")
self.config.parser.modules.add("headers_module")
mock_exe.return_value = True
# This will create an ssl vhost for certbot.demo
@@ -1011,9 +1042,9 @@ class MultipleVhostsTest(util.ApacheTest):
self.assertEqual(len(hsts_header), 4)
def test_http_header_hsts_twice(self):
self.config.parser.modules["mod_ssl.c"] = None
self.config.parser.modules.add("mod_ssl.c")
# skip the enable mod
self.config.parser.modules["headers_module"] = None
self.config.parser.modules.add("headers_module")
# This will create an ssl vhost for encryption-example.demo
self.config.choose_vhost("encryption-example.demo")
@@ -1029,8 +1060,8 @@ class MultipleVhostsTest(util.ApacheTest):
@mock.patch("certbot.util.exe_exists")
def test_http_header_uir(self, mock_exe, _):
self.config.parser.update_runtime_variables = mock.Mock()
self.config.parser.modules["mod_ssl.c"] = None
self.config.parser.modules["headers_module"] = None
self.config.parser.modules.add("mod_ssl.c")
self.config.parser.modules.add("headers_module")
mock_exe.return_value = True
@@ -1053,9 +1084,9 @@ class MultipleVhostsTest(util.ApacheTest):
self.assertEqual(len(uir_header), 4)
def test_http_header_uir_twice(self):
self.config.parser.modules["mod_ssl.c"] = None
self.config.parser.modules.add("mod_ssl.c")
# skip the enable mod
self.config.parser.modules["headers_module"] = None
self.config.parser.modules.add("headers_module")
# This will create an ssl vhost for encryption-example.demo
self.config.choose_vhost("encryption-example.demo")
@@ -1070,7 +1101,7 @@ class MultipleVhostsTest(util.ApacheTest):
@mock.patch("certbot.util.run_script")
@mock.patch("certbot.util.exe_exists")
def test_redirect_well_formed_http(self, mock_exe, _):
self.config.parser.modules["rewrite_module"] = None
self.config.parser.modules.add("rewrite_module")
self.config.parser.update_runtime_variables = mock.Mock()
mock_exe.return_value = True
self.config.get_version = mock.Mock(return_value=(2, 2))
@@ -1096,7 +1127,7 @@ class MultipleVhostsTest(util.ApacheTest):
def test_rewrite_rule_exists(self):
# Skip the enable mod
self.config.parser.modules["rewrite_module"] = None
self.config.parser.modules.add("rewrite_module")
self.config.get_version = mock.Mock(return_value=(2, 3, 9))
self.config.parser.add_dir(
self.vh_truth[3].path, "RewriteRule", ["Unknown"])
@@ -1105,7 +1136,7 @@ class MultipleVhostsTest(util.ApacheTest):
def test_rewrite_engine_exists(self):
# Skip the enable mod
self.config.parser.modules["rewrite_module"] = None
self.config.parser.modules.add("rewrite_module")
self.config.get_version = mock.Mock(return_value=(2, 3, 9))
self.config.parser.add_dir(
self.vh_truth[3].path, "RewriteEngine", "on")
@@ -1115,7 +1146,7 @@ class MultipleVhostsTest(util.ApacheTest):
@mock.patch("certbot.util.run_script")
@mock.patch("certbot.util.exe_exists")
def test_redirect_with_existing_rewrite(self, mock_exe, _):
self.config.parser.modules["rewrite_module"] = None
self.config.parser.modules.add("rewrite_module")
self.config.parser.update_runtime_variables = mock.Mock()
mock_exe.return_value = True
self.config.get_version = mock.Mock(return_value=(2, 2, 0))
@@ -1149,7 +1180,7 @@ class MultipleVhostsTest(util.ApacheTest):
@mock.patch("certbot.util.run_script")
@mock.patch("certbot.util.exe_exists")
def test_redirect_with_old_https_redirection(self, mock_exe, _):
self.config.parser.modules["rewrite_module"] = None
self.config.parser.modules.add("rewrite_module")
self.config.parser.update_runtime_variables = mock.Mock()
mock_exe.return_value = True
self.config.get_version = mock.Mock(return_value=(2, 2, 0))
@@ -1178,10 +1209,10 @@ class MultipleVhostsTest(util.ApacheTest):
def test_redirect_with_conflict(self):
self.config.parser.modules["rewrite_module"] = None
self.config.parser.modules.add("rewrite_module")
ssl_vh = obj.VirtualHost(
"fp", "ap", {obj.Addr(("*", "443")),
obj.Addr(("zombo.com",))},
"fp", "ap", set([obj.Addr(("*", "443")),
obj.Addr(("zombo.com",))]),
True, False)
# No names ^ this guy should conflict.
@@ -1191,7 +1222,7 @@ class MultipleVhostsTest(util.ApacheTest):
def test_redirect_two_domains_one_vhost(self):
# Skip the enable mod
self.config.parser.modules["rewrite_module"] = None
self.config.parser.modules.add("rewrite_module")
self.config.get_version = mock.Mock(return_value=(2, 3, 9))
# Creates ssl vhost for the domain
@@ -1206,7 +1237,7 @@ class MultipleVhostsTest(util.ApacheTest):
def test_redirect_from_previous_run(self):
# Skip the enable mod
self.config.parser.modules["rewrite_module"] = None
self.config.parser.modules.add("rewrite_module")
self.config.get_version = mock.Mock(return_value=(2, 3, 9))
self.config.choose_vhost("red.blue.purple.com")
self.config.enhance("red.blue.purple.com", "redirect")
@@ -1219,22 +1250,22 @@ class MultipleVhostsTest(util.ApacheTest):
self.config.enhance, "green.blue.purple.com", "redirect")
def test_create_own_redirect(self):
self.config.parser.modules["rewrite_module"] = None
self.config.parser.modules.add("rewrite_module")
self.config.get_version = mock.Mock(return_value=(2, 3, 9))
# For full testing... give names...
self.vh_truth[1].name = "default.com"
self.vh_truth[1].aliases = {"yes.default.com"}
self.vh_truth[1].aliases = set(["yes.default.com"])
# pylint: disable=protected-access
self.config._enable_redirect(self.vh_truth[1], "")
self.assertEqual(len(self.config.vhosts), 13)
def test_create_own_redirect_for_old_apache_version(self):
self.config.parser.modules["rewrite_module"] = None
self.config.parser.modules.add("rewrite_module")
self.config.get_version = mock.Mock(return_value=(2, 2))
# For full testing... give names...
self.vh_truth[1].name = "default.com"
self.vh_truth[1].aliases = {"yes.default.com"}
self.vh_truth[1].aliases = set(["yes.default.com"])
# pylint: disable=protected-access
self.config._enable_redirect(self.vh_truth[1], "")
@@ -1292,13 +1323,12 @@ class MultipleVhostsTest(util.ApacheTest):
os.path.basename(inc_path) in self.config.parser.existing_paths[
os.path.dirname(inc_path)])
@mock.patch('certbot_apache._internal.configurator.display_util.notify')
def test_deploy_cert_not_parsed_path(self, unused_mock_notify):
def test_deploy_cert_not_parsed_path(self):
# Make sure that we add include to root config for vhosts when
# handle-sites is false
self.config.parser.modules["ssl_module"] = None
self.config.parser.modules["mod_ssl.c"] = None
self.config.parser.modules["socache_shmcb_module"] = None
self.config.parser.modules.add("ssl_module")
self.config.parser.modules.add("mod_ssl.c")
self.config.parser.modules.add("socache_shmcb_module")
tmp_path = filesystem.realpath(tempfile.mkdtemp("vhostroot"))
filesystem.chmod(tmp_path, 0o755)
mock_p = "certbot_apache._internal.configurator.ApacheConfigurator._get_ssl_vhost_path"
@@ -1315,16 +1345,6 @@ class MultipleVhostsTest(util.ApacheTest):
self.assertTrue(mock_add.called)
shutil.rmtree(tmp_path)
def test_deploy_cert_no_mod_ssl(self):
# Create
ssl_vhost = self.config.make_vhost_ssl(self.vh_truth[0])
self.config.parser.modules["socache_shmcb_module"] = None
self.config.prepare_server_https = mock.Mock()
self.assertRaises(errors.MisconfigurationError, self.config.deploy_cert,
"encryption-example.demo", "example/cert.pem", "example/key.pem",
"example/cert_chain.pem", "example/fullchain.pem")
@mock.patch("certbot_apache._internal.parser.ApacheParser.parsed_in_original")
def test_choose_vhost_and_servername_addition_parsed(self, mock_parsed):
ret_vh = self.vh_truth[8]
@@ -1338,6 +1358,13 @@ class MultipleVhostsTest(util.ApacheTest):
self.config.enable_mod,
"whatever")
def test_wildcard_domain(self):
# pylint: disable=protected-access
cases = {u"*.example.org": True, b"*.x.example.org": True,
u"a.example.org": False, b"a.x.example.org": False}
for key in cases:
self.assertEqual(self.config._wildcard_domain(key), cases[key])
def test_choose_vhosts_wildcard(self):
# pylint: disable=protected-access
mock_path = "certbot_apache._internal.display_ops.select_vhost_multiple"
@@ -1351,10 +1378,10 @@ class MultipleVhostsTest(util.ApacheTest):
# And the actual returned values
self.assertEqual(len(vhs), 1)
self.assertEqual(vhs[0].name, "certbot.demo")
self.assertTrue(vhs[0].name == "certbot.demo")
self.assertTrue(vhs[0].ssl)
self.assertNotEqual(vhs[0], self.vh_truth[3])
self.assertFalse(vhs[0] == self.vh_truth[3])
@mock.patch("certbot_apache._internal.configurator.ApacheConfigurator.make_vhost_ssl")
def test_choose_vhosts_wildcard_no_ssl(self, mock_makessl):
@@ -1389,8 +1416,7 @@ class MultipleVhostsTest(util.ApacheTest):
self.assertEqual(vhs[0], self.vh_truth[7])
@mock.patch('certbot_apache._internal.configurator.display_util.notify')
def test_deploy_cert_wildcard(self, unused_mock_notify):
def test_deploy_cert_wildcard(self):
# pylint: disable=protected-access
mock_choose_vhosts = mock.MagicMock()
mock_choose_vhosts.return_value = [self.vh_truth[7]]
@@ -1415,8 +1441,8 @@ class MultipleVhostsTest(util.ApacheTest):
@mock.patch("certbot_apache._internal.configurator.ApacheConfigurator._choose_vhosts_wildcard")
def test_enhance_wildcard_after_install(self, mock_choose):
# pylint: disable=protected-access
self.config.parser.modules["mod_ssl.c"] = None
self.config.parser.modules["headers_module"] = None
self.config.parser.modules.add("mod_ssl.c")
self.config.parser.modules.add("headers_module")
self.vh_truth[3].ssl = True
self.config._wildcard_vhosts["*.certbot.demo"] = [self.vh_truth[3]]
self.config.enhance("*.certbot.demo", "ensure-http-header",
@@ -1427,8 +1453,8 @@ class MultipleVhostsTest(util.ApacheTest):
def test_enhance_wildcard_no_install(self, mock_choose):
self.vh_truth[3].ssl = True
mock_choose.return_value = [self.vh_truth[3]]
self.config.parser.modules["mod_ssl.c"] = None
self.config.parser.modules["headers_module"] = None
self.config.parser.modules.add("mod_ssl.c")
self.config.parser.modules.add("headers_module")
self.config.enhance("*.certbot.demo", "ensure-http-header",
"Upgrade-Insecure-Requests")
self.assertTrue(mock_choose.called)
@@ -1466,10 +1492,10 @@ class MultipleVhostsTest(util.ApacheTest):
self.config.parser.aug.match = mock_match
vhs = self.config.get_virtual_hosts()
self.assertEqual(len(vhs), 2)
self.assertEqual(vhs[0], self.vh_truth[1])
self.assertTrue(vhs[0] == self.vh_truth[1])
# mock_vhost should have replaced the vh_truth[0], because its filepath
# isn't a symlink
self.assertEqual(vhs[1], mock_vhost)
self.assertTrue(vhs[1] == mock_vhost)
class AugeasVhostsTest(util.ApacheTest):
@@ -1480,9 +1506,9 @@ class AugeasVhostsTest(util.ApacheTest):
td = "debian_apache_2_4/augeas_vhosts"
cr = "debian_apache_2_4/augeas_vhosts/apache2"
vr = "debian_apache_2_4/augeas_vhosts/apache2/sites-available"
super().setUp(test_dir=td,
config_root=cr,
vhost_root=vr)
super(AugeasVhostsTest, self).setUp(test_dir=td,
config_root=cr,
vhost_root=vr)
self.config = util.get_apache_configurator(
self.config_path, self.vhost_path, self.config_dir,
@@ -1559,9 +1585,9 @@ class MultiVhostsTest(util.ApacheTest):
td = "debian_apache_2_4/multi_vhosts"
cr = "debian_apache_2_4/multi_vhosts/apache2"
vr = "debian_apache_2_4/multi_vhosts/apache2/sites-available"
super().setUp(test_dir=td,
config_root=cr,
vhost_root=vr)
super(MultiVhostsTest, self).setUp(test_dir=td,
config_root=cr,
vhost_root=vr)
self.config = util.get_apache_configurator(
self.config_path, self.vhost_path,
@@ -1580,7 +1606,7 @@ class MultiVhostsTest(util.ApacheTest):
self.assertEqual(ssl_vhost.path,
"/files" + ssl_vhost.filep + "/IfModule/VirtualHost")
self.assertEqual(len(ssl_vhost.addrs), 1)
self.assertEqual({obj.Addr.fromstring("*:443")}, ssl_vhost.addrs)
self.assertEqual(set([obj.Addr.fromstring("*:443")]), ssl_vhost.addrs)
self.assertEqual(ssl_vhost.name, "banana.vomit.com")
self.assertTrue(ssl_vhost.ssl)
self.assertFalse(ssl_vhost.enabled)
@@ -1610,9 +1636,9 @@ class MultiVhostsTest(util.ApacheTest):
self.assertEqual(self.config._get_new_vh_path(without_index, both),
with_index_2[0])
@mock.patch("certbot_apache._internal.configurator.display_util.notify")
def test_make_vhost_ssl_with_existing_rewrite_rule(self, mock_notify):
self.config.parser.modules["rewrite_module"] = None
@certbot_util.patch_get_utility()
def test_make_vhost_ssl_with_existing_rewrite_rule(self, mock_get_utility):
self.config.parser.modules.add("rewrite_module")
ssl_vhost = self.config.make_vhost_ssl(self.vh_truth[4])
@@ -1627,12 +1653,12 @@ class MultiVhostsTest(util.ApacheTest):
"\"http://new.example.com/docs/$1\" [R,L]")
self.assertTrue(commented_rewrite_rule in conf_text)
self.assertTrue(uncommented_rewrite_rule in conf_text)
self.assertEqual(mock_notify.call_count, 1)
self.assertIn("Some rewrite rules", mock_notify.call_args[0][0])
mock_get_utility().add_message.assert_called_once_with(mock.ANY,
mock.ANY)
@mock.patch("certbot_apache._internal.configurator.display_util.notify")
def test_make_vhost_ssl_with_existing_rewrite_conds(self, mock_notify):
self.config.parser.modules["rewrite_module"] = None
@certbot_util.patch_get_utility()
def test_make_vhost_ssl_with_existing_rewrite_conds(self, mock_get_utility):
self.config.parser.modules.add("rewrite_module")
ssl_vhost = self.config.make_vhost_ssl(self.vh_truth[3])
@@ -1656,15 +1682,15 @@ class MultiVhostsTest(util.ApacheTest):
self.assertTrue(commented_cond1 in conf_line_set)
self.assertTrue(commented_cond2 in conf_line_set)
self.assertTrue(commented_rewrite_rule in conf_line_set)
self.assertEqual(mock_notify.call_count, 1)
self.assertIn("Some rewrite rules", mock_notify.call_args[0][0])
mock_get_utility().add_message.assert_called_once_with(mock.ANY,
mock.ANY)
class InstallSslOptionsConfTest(util.ApacheTest):
"""Test that the options-ssl-nginx.conf file is installed and updated properly."""
def setUp(self): # pylint: disable=arguments-differ
super().setUp()
super(InstallSslOptionsConfTest, self).setUp()
self.config = util.get_apache_configurator(
self.config_path, self.vhost_path, self.config_dir, self.work_dir)
@@ -1674,7 +1700,7 @@ class InstallSslOptionsConfTest(util.ApacheTest):
self.config.updated_mod_ssl_conf_digest)
def _current_ssl_options_hash(self):
return crypto_util.sha256sum(self.config.pick_apache_config())
return crypto_util.sha256sum(self.config.option("MOD_SSL_CONF_SRC"))
def _assert_current_file(self):
self.assertTrue(os.path.isfile(self.config.mod_ssl_conf))
@@ -1710,7 +1736,7 @@ class InstallSslOptionsConfTest(util.ApacheTest):
self.assertFalse(mock_logger.warning.called)
self.assertTrue(os.path.isfile(self.config.mod_ssl_conf))
self.assertEqual(crypto_util.sha256sum(
self.config.pick_apache_config()),
self.config.option("MOD_SSL_CONF_SRC")),
self._current_ssl_options_hash())
self.assertNotEqual(crypto_util.sha256sum(self.config.mod_ssl_conf),
self._current_ssl_options_hash())
@@ -1726,118 +1752,19 @@ class InstallSslOptionsConfTest(util.ApacheTest):
"%s has been manually modified; updated file "
"saved to %s. We recommend updating %s for security purposes.")
self.assertEqual(crypto_util.sha256sum(
self.config.pick_apache_config()),
self.config.option("MOD_SSL_CONF_SRC")),
self._current_ssl_options_hash())
# only print warning once
with mock.patch("certbot.plugins.common.logger") as mock_logger:
self._call()
self.assertFalse(mock_logger.warning.called)
def test_ssl_config_files_hash_in_all_hashes(self):
"""
It is really critical that all TLS Apache config files have their SHA256 hash registered in
constants.ALL_SSL_OPTIONS_HASHES. Otherwise Certbot will mistakenly assume that the config
file has been manually edited by the user, and will refuse to update it.
This test ensures that all necessary hashes are present.
"""
def test_current_file_hash_in_all_hashes(self):
from certbot_apache._internal.constants import ALL_SSL_OPTIONS_HASHES
import pkg_resources
self.assertTrue(self._current_ssl_options_hash() in ALL_SSL_OPTIONS_HASHES,
"Constants.ALL_SSL_OPTIONS_HASHES must be appended"
" with the sha256 hash of self.config.mod_ssl_conf when it is updated.")
tls_configs_dir = pkg_resources.resource_filename(
"certbot_apache", os.path.join("_internal", "tls_configs"))
all_files = [os.path.join(tls_configs_dir, name) for name in os.listdir(tls_configs_dir)
if name.endswith('options-ssl-apache.conf')]
self.assertTrue(all_files)
for one_file in all_files:
file_hash = crypto_util.sha256sum(one_file)
self.assertTrue(file_hash in ALL_SSL_OPTIONS_HASHES,
"Constants.ALL_SSL_OPTIONS_HASHES must be appended with the sha256 "
"hash of {0} when it is updated.".format(one_file))
def test_openssl_version(self):
self.config._openssl_version = None
some_string_contents = b"""
SSLOpenSSLConfCmd
OpenSSL configuration command
SSLv3 not supported by this version of OpenSSL
'%s': invalid OpenSSL configuration command
OpenSSL 1.0.2g 1 Mar 2016
OpenSSL
AH02407: "SSLOpenSSLConfCmd %s %s" failed for %s
AH02556: "SSLOpenSSLConfCmd %s %s" applied to %s
OpenSSL 1.0.2g 1 Mar 2016
"""
# ssl_module as a DSO
self.config.parser.modules['ssl_module'] = '/fake/path'
with mock.patch("certbot_apache._internal.configurator."
"ApacheConfigurator._open_module_file") as mock_omf:
mock_omf.return_value = some_string_contents
self.assertEqual(self.config.openssl_version(), "1.0.2g")
# ssl_module statically linked
self.config._openssl_version = None
self.config.parser.modules['ssl_module'] = None
self.config.options.bin = '/fake/path/to/httpd'
with mock.patch("certbot_apache._internal.configurator."
"ApacheConfigurator._open_module_file") as mock_omf:
mock_omf.return_value = some_string_contents
self.assertEqual(self.config.openssl_version(), "1.0.2g")
def test_current_version(self):
self.config.version = (2, 4, 10)
self.config._openssl_version = '1.0.2m'
self.assertTrue('old' in self.config.pick_apache_config())
self.config.version = (2, 4, 11)
self.config._openssl_version = '1.0.2m'
self.assertTrue('current' in self.config.pick_apache_config())
self.config._openssl_version = '1.0.2a'
self.assertTrue('old' in self.config.pick_apache_config())
def test_openssl_version_warns(self):
self.config._openssl_version = '1.0.2a'
self.assertEqual(self.config.openssl_version(), '1.0.2a')
self.config._openssl_version = None
with mock.patch("certbot_apache._internal.configurator.logger.warning") as mock_log:
self.assertEqual(self.config.openssl_version(), None)
self.assertTrue("Could not find ssl_module" in mock_log.call_args[0][0])
# When no ssl_module is present at all
self.config._openssl_version = None
self.assertTrue("ssl_module" not in self.config.parser.modules)
with mock.patch("certbot_apache._internal.configurator.logger.warning") as mock_log:
self.assertEqual(self.config.openssl_version(), None)
self.assertTrue("Could not find ssl_module" in mock_log.call_args[0][0])
# When ssl_module is statically linked but --apache-bin not provided
self.config._openssl_version = None
self.config.options.bin = None
self.config.parser.modules['ssl_module'] = None
with mock.patch("certbot_apache._internal.configurator.logger.warning") as mock_log:
self.assertEqual(self.config.openssl_version(), None)
self.assertTrue("ssl_module is statically linked but" in mock_log.call_args[0][0])
self.config.parser.modules['ssl_module'] = "/fake/path"
with mock.patch("certbot_apache._internal.configurator.logger.warning") as mock_log:
# Check that correct logger.warning was printed
self.assertEqual(self.config.openssl_version(), None)
self.assertTrue("Unable to read" in mock_log.call_args[0][0])
contents_missing_openssl = b"these contents won't match the regex"
with mock.patch("certbot_apache._internal.configurator."
"ApacheConfigurator._open_module_file") as mock_omf:
mock_omf.return_value = contents_missing_openssl
with mock.patch("certbot_apache._internal.configurator.logger.warning") as mock_log:
# Check that correct logger.warning was printed
self.assertEqual(self.config.openssl_version(), None)
self.assertTrue("Could not find OpenSSL" in mock_log.call_args[0][0])
def test_open_module_file(self):
mock_open = mock.mock_open(read_data="testing 12 3")
with mock.patch("builtins.open", mock_open):
self.assertEqual(self.config._open_module_file("/nonsense/"), "testing 12 3")
if __name__ == "__main__":
unittest.main() # pragma: no cover

View File

@@ -2,10 +2,7 @@
import shutil
import unittest
try:
import mock
except ImportError: # pragma: no cover
from unittest import mock # type: ignore
import mock
from certbot import errors
from certbot.compat import os
@@ -20,7 +17,7 @@ class MultipleVhostsTestDebian(util.ApacheTest):
_multiprocess_can_split_ = True
def setUp(self): # pylint: disable=arguments-differ
super().setUp()
super(MultipleVhostsTestDebian, self).setUp()
self.config = util.get_apache_configurator(
self.config_path, self.vhost_path, self.config_dir, self.work_dir,
os_info="debian")
@@ -49,11 +46,10 @@ class MultipleVhostsTestDebian(util.ApacheTest):
@mock.patch("certbot.util.run_script")
@mock.patch("certbot.util.exe_exists")
@mock.patch("certbot_apache._internal.apache_util.subprocess.run")
def test_enable_mod(self, mock_run, mock_exe_exists, mock_run_script):
mock_run.return_value.stdout = "Define: DUMP_RUN_CFG"
mock_run.return_value.stderr = ""
mock_run.return_value.returncode = 0
@mock.patch("certbot_apache._internal.apache_util.subprocess.Popen")
def test_enable_mod(self, mock_popen, mock_exe_exists, mock_run_script):
mock_popen().communicate.return_value = ("Define: DUMP_RUN_CFG", "")
mock_popen().returncode = 0
mock_exe_exists.return_value = True
self.config.enable_mod("ssl")
@@ -65,8 +61,8 @@ class MultipleVhostsTestDebian(util.ApacheTest):
def test_deploy_cert_enable_new_vhost(self):
# Create
ssl_vhost = self.config.make_vhost_ssl(self.vh_truth[0])
self.config.parser.modules["ssl_module"] = None
self.config.parser.modules["mod_ssl.c"] = None
self.config.parser.modules.add("ssl_module")
self.config.parser.modules.add("mod_ssl.c")
self.assertFalse(ssl_vhost.enabled)
self.config.deploy_cert(
"encryption-example.demo", "example/cert.pem", "example/key.pem",
@@ -96,8 +92,8 @@ class MultipleVhostsTestDebian(util.ApacheTest):
self.config_path, self.vhost_path, self.config_dir,
self.work_dir, version=(2, 4, 16))
self.config = self.mock_deploy_cert(self.config)
self.config.parser.modules["ssl_module"] = None
self.config.parser.modules["mod_ssl.c"] = None
self.config.parser.modules.add("ssl_module")
self.config.parser.modules.add("mod_ssl.c")
# Get the default 443 vhost
self.config.assoc["random.demo"] = self.vh_truth[1]
@@ -132,8 +128,8 @@ class MultipleVhostsTestDebian(util.ApacheTest):
self.config_path, self.vhost_path, self.config_dir,
self.work_dir, version=(2, 4, 16))
self.config = self.mock_deploy_cert(self.config)
self.config.parser.modules["ssl_module"] = None
self.config.parser.modules["mod_ssl.c"] = None
self.config.parser.modules.add("ssl_module")
self.config.parser.modules.add("mod_ssl.c")
# Get the default 443 vhost
self.config.assoc["random.demo"] = self.vh_truth[1]
@@ -147,8 +143,8 @@ class MultipleVhostsTestDebian(util.ApacheTest):
self.config_path, self.vhost_path, self.config_dir,
self.work_dir, version=(2, 4, 7))
self.config = self.mock_deploy_cert(self.config)
self.config.parser.modules["ssl_module"] = None
self.config.parser.modules["mod_ssl.c"] = None
self.config.parser.modules.add("ssl_module")
self.config.parser.modules.add("mod_ssl.c")
# Get the default 443 vhost
self.config.assoc["random.demo"] = self.vh_truth[1]
@@ -161,7 +157,7 @@ class MultipleVhostsTestDebian(util.ApacheTest):
@mock.patch("certbot.util.exe_exists")
def test_ocsp_stapling_enable_mod(self, mock_exe, _):
self.config.parser.update_runtime_variables = mock.Mock()
self.config.parser.modules["mod_ssl.c"] = None
self.config.parser.modules.add("mod_ssl.c")
self.config.get_version = mock.Mock(return_value=(2, 4, 7))
mock_exe.return_value = True
# This will create an ssl vhost for certbot.demo
@@ -173,7 +169,7 @@ class MultipleVhostsTestDebian(util.ApacheTest):
@mock.patch("certbot.util.exe_exists")
def test_ensure_http_header_enable_mod(self, mock_exe, _):
self.config.parser.update_runtime_variables = mock.Mock()
self.config.parser.modules["mod_ssl.c"] = None
self.config.parser.modules.add("mod_ssl.c")
mock_exe.return_value = True
# This will create an ssl vhost for certbot.demo

View File

@@ -1,10 +1,7 @@
"""Test certbot_apache._internal.display_ops."""
import unittest
try:
import mock
except ImportError: # pragma: no cover
from unittest import mock # type: ignore
import mock
from certbot import errors
from certbot.display import util as display_util
@@ -96,9 +93,9 @@ class SelectVhostTest(unittest.TestCase):
self.vhosts.append(
obj.VirtualHost(
"path", "aug_path", {obj.Addr.fromstring("*:80")},
"path", "aug_path", set([obj.Addr.fromstring("*:80")]),
False, False,
"wildcard.com", {"*.wildcard.com"}))
"wildcard.com", set(["*.wildcard.com"])))
self.assertEqual(self.vhosts[5], self._call(self.vhosts))

View File

@@ -1,10 +1,7 @@
"""Tests for DualParserNode implementation"""
import unittest
try:
import mock
except ImportError: # pragma: no cover
from unittest import mock # type: ignore
import mock
from certbot_apache._internal import assertions
from certbot_apache._internal import augeasparser
@@ -18,7 +15,11 @@ class DualParserNodeTest(unittest.TestCase): # pylint: disable=too-many-public-
parser_mock = mock.MagicMock()
parser_mock.aug.match.return_value = []
parser_mock.get_arg.return_value = []
self.metadata = {"augeasparser": parser_mock, "augeaspath": "/invalid", "ac_ast": None}
ast_mock = mock.MagicMock()
ast_mock.__iter__.return_value = iter([])
self.metadata = {"augeasparser": parser_mock,
"augeaspath": "/invalid", "ac_ast": ast_mock,
"apache_vars": { "modules": set(), "defines": set(), "includes": set(), }}
self.block = dualparser.DualBlockNode(name="block",
ancestor=None,
filepath="/tmp/something",
@@ -412,9 +413,9 @@ class DualParserNodeTest(unittest.TestCase): # pylint: disable=too-many-public-
ancestor=self.block,
filepath="/path/to/whatever",
metadata=self.metadata)
self.assertNotEqual(self.block, ne_block)
self.assertNotEqual(self.directive, ne_directive)
self.assertNotEqual(self.comment, ne_comment)
self.assertFalse(self.block == ne_block)
self.assertFalse(self.directive == ne_directive)
self.assertFalse(self.comment == ne_comment)
def test_parsed_paths(self):
mock_p = mock.MagicMock(return_value=['/path/file.conf',

View File

@@ -1,10 +1,7 @@
"""Test for certbot_apache._internal.entrypoint for override class resolution"""
import unittest
try:
import mock
except ImportError: # pragma: no cover
from unittest import mock # type: ignore
import mock
from certbot_apache._internal import configurator
from certbot_apache._internal import entrypoint

View File

@@ -1,10 +1,7 @@
"""Test for certbot_apache._internal.configurator for Fedora 29+ overrides"""
import unittest
try:
import mock
except ImportError: # pragma: no cover
from unittest import mock # type: ignore
import mock
from certbot import errors
from certbot.compat import filesystem
@@ -46,9 +43,9 @@ class FedoraRestartTest(util.ApacheTest):
test_dir = "centos7_apache/apache"
config_root = "centos7_apache/apache/httpd"
vhost_root = "centos7_apache/apache/httpd/conf.d"
super().setUp(test_dir=test_dir,
config_root=config_root,
vhost_root=vhost_root)
super(FedoraRestartTest, self).setUp(test_dir=test_dir,
config_root=config_root,
vhost_root=vhost_root)
self.config = util.get_apache_configurator(
self.config_path, self.vhost_path, self.config_dir, self.work_dir,
os_info="fedora")
@@ -90,9 +87,9 @@ class MultipleVhostsTestFedora(util.ApacheTest):
test_dir = "centos7_apache/apache"
config_root = "centos7_apache/apache/httpd"
vhost_root = "centos7_apache/apache/httpd/conf.d"
super().setUp(test_dir=test_dir,
config_root=config_root,
vhost_root=vhost_root)
super(MultipleVhostsTestFedora, self).setUp(test_dir=test_dir,
config_root=config_root,
vhost_root=vhost_root)
self.config = util.get_apache_configurator(
self.config_path, self.vhost_path, self.config_dir, self.work_dir,
@@ -123,7 +120,7 @@ class MultipleVhostsTestFedora(util.ApacheTest):
return mod_val
return ""
mock_get.side_effect = mock_get_cfg
self.config.parser.modules = {}
self.config.parser.modules = set()
self.config.parser.variables = {}
with mock.patch("certbot.util.get_os_info") as mock_osi:
@@ -134,7 +131,7 @@ class MultipleVhostsTestFedora(util.ApacheTest):
self.assertEqual(mock_get.call_count, 3)
self.assertEqual(len(self.config.parser.modules), 4)
self.assertEqual(len(self.config.parser.variables), 2)
self.assertTrue("TEST2" in self.config.parser.variables)
self.assertTrue("TEST2" in self.config.parser.variables.keys())
self.assertTrue("mod_another.c" in self.config.parser.modules)
@mock.patch("certbot_apache._internal.configurator.util.run_script")
@@ -172,11 +169,11 @@ class MultipleVhostsTestFedora(util.ApacheTest):
mock_osi.return_value = ("fedora", "29")
self.config.parser.update_runtime_variables()
self.assertTrue("mock_define" in self.config.parser.variables)
self.assertTrue("mock_define_too" in self.config.parser.variables)
self.assertTrue("mock_value" in self.config.parser.variables)
self.assertTrue("mock_define" in self.config.parser.variables.keys())
self.assertTrue("mock_define_too" in self.config.parser.variables.keys())
self.assertTrue("mock_value" in self.config.parser.variables.keys())
self.assertEqual("TRUE", self.config.parser.variables["mock_value"])
self.assertTrue("MOCK_NOSEP" in self.config.parser.variables)
self.assertTrue("MOCK_NOSEP" in self.config.parser.variables.keys())
self.assertEqual("NOSEP_VAL", self.config.parser.variables["NOSEP_TWO"])
@mock.patch("certbot_apache._internal.configurator.util.run_script")

View File

@@ -1,10 +1,7 @@
"""Test for certbot_apache._internal.configurator for Gentoo overrides"""
import unittest
try:
import mock
except ImportError: # pragma: no cover
from unittest import mock # type: ignore
import mock
from certbot import errors
from certbot.compat import filesystem
@@ -24,19 +21,19 @@ def get_vh_truth(temp_dir, config_name):
obj.VirtualHost(
os.path.join(prefix, "gentoo.example.com.conf"),
os.path.join(aug_pre, "gentoo.example.com.conf/VirtualHost"),
{obj.Addr.fromstring("*:80")},
set([obj.Addr.fromstring("*:80")]),
False, True, "gentoo.example.com"),
obj.VirtualHost(
os.path.join(prefix, "00_default_vhost.conf"),
os.path.join(aug_pre, "00_default_vhost.conf/IfDefine/VirtualHost"),
{obj.Addr.fromstring("*:80")},
set([obj.Addr.fromstring("*:80")]),
False, True, "localhost"),
obj.VirtualHost(
os.path.join(prefix, "00_default_ssl_vhost.conf"),
os.path.join(aug_pre,
"00_default_ssl_vhost.conf" +
"/IfDefine/IfDefine/IfModule/VirtualHost"),
{obj.Addr.fromstring("_default_:443")},
set([obj.Addr.fromstring("_default_:443")]),
True, True, "localhost")
]
return vh_truth
@@ -50,9 +47,9 @@ class MultipleVhostsTestGentoo(util.ApacheTest):
test_dir = "gentoo_apache/apache"
config_root = "gentoo_apache/apache/apache2"
vhost_root = "gentoo_apache/apache/apache2/vhosts.d"
super().setUp(test_dir=test_dir,
config_root=config_root,
vhost_root=vhost_root)
super(MultipleVhostsTestGentoo, self).setUp(test_dir=test_dir,
config_root=config_root,
vhost_root=vhost_root)
# pylint: disable=line-too-long
with mock.patch("certbot_apache._internal.override_gentoo.GentooParser.update_runtime_variables"):
@@ -91,7 +88,7 @@ class MultipleVhostsTestGentoo(util.ApacheTest):
with mock.patch("certbot_apache._internal.override_gentoo.GentooParser.update_modules"):
self.config.parser.update_runtime_variables()
for define in defines:
self.assertTrue(define in self.config.parser.variables)
self.assertTrue(define in self.config.parser.variables.keys())
@mock.patch("certbot_apache._internal.apache_util.parse_from_subprocess")
def test_no_binary_configdump(self, mock_subprocess):
@@ -120,7 +117,7 @@ class MultipleVhostsTestGentoo(util.ApacheTest):
return mod_val
return None # pragma: no cover
mock_get.side_effect = mock_get_cfg
self.config.parser.modules = {}
self.config.parser.modules = set()
with mock.patch("certbot.util.get_os_info") as mock_osi:
# Make sure we have the have the Gentoo httpd constants

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